add skel plugin for code sharing (#473)
* introduce plugin skel to reuse code * Refactor code to use libplugin.NewSkelPlugin for plugin/kubernetes/main.go Add YAML Plugin skel.go for plugin/yaml * Fix code scanning alert no. 6: Incorrect conversion between integer types Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix gpt * fjx password handling in SkelPlugin and skelpipeToWrapper * go fmt * Refactor Docker plugin to use skelpipe wrapper * Refactor skel.go to use container username instead of client username * revert yaml test order * fix public and password mess up * Refactor working dir to use skel * revert deleted file * Refactor skel.go to remove unused code and simplify MatchConn function * Refactor skel.go to read userKnownHosts file in KnownHosts function * remove workingdirbykey from goreleaser * Refactor workingdir.go to use libplugin.SplitHostPortForSSH for parsing host and port * Refactor skel.go to remove unused code and simplify MatchConn function * merge doc into workingdir --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
parent
adc5d51c33
commit
14ceadb8e8
18 changed files with 1200 additions and 819 deletions
|
|
@ -8,7 +8,6 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
|
||||
plugin := newYamlPlugin()
|
||||
|
||||
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
|
||||
|
|
@ -30,25 +29,8 @@ func main() {
|
|||
},
|
||||
},
|
||||
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
|
||||
|
||||
return &libplugin.SshPiperPluginConfig{
|
||||
|
||||
NextAuthMethodsCallback: func(_ libplugin.ConnMetadata) ([]string, error) {
|
||||
return plugin.supportedMethods()
|
||||
},
|
||||
|
||||
PasswordCallback: func(conn libplugin.ConnMetadata, password []byte) (*libplugin.Upstream, error) {
|
||||
return plugin.findAndCreateUpstream(conn, string(password), nil)
|
||||
},
|
||||
|
||||
PublicKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (*libplugin.Upstream, error) {
|
||||
return plugin.findAndCreateUpstream(conn, "", key)
|
||||
},
|
||||
|
||||
VerifyHostKeyCallback: func(conn libplugin.ConnMetadata, hostname, netaddr string, key []byte) error {
|
||||
return plugin.verifyHostKey(conn, hostname, netaddr, key)
|
||||
},
|
||||
}, nil
|
||||
skel := libplugin.NewSkelPlugin(plugin.listPipe)
|
||||
return skel.CreateConfig(), nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
163
plugin/yaml/skel.go
Normal file
163
plugin/yaml/skel.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
//go:build full || e2e
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/tg123/sshpiper/libplugin"
|
||||
)
|
||||
|
||||
type skelpipeWrapper struct {
|
||||
plugin *plugin
|
||||
|
||||
pipe *yamlPipe
|
||||
}
|
||||
type skelpipeFromWrapper struct {
|
||||
plugin *plugin
|
||||
|
||||
from *yamlPipeFrom
|
||||
to *yamlPipeTo
|
||||
}
|
||||
type skelpipePasswordWrapper struct {
|
||||
skelpipeFromWrapper
|
||||
}
|
||||
|
||||
type skelpipePublicKeyWrapper struct {
|
||||
skelpipeFromWrapper
|
||||
}
|
||||
|
||||
type skelpipeToWrapper struct {
|
||||
plugin *plugin
|
||||
|
||||
username string
|
||||
to *yamlPipeTo
|
||||
}
|
||||
|
||||
func (s *skelpipeWrapper) From() []libplugin.SkelPipeFrom {
|
||||
var froms []libplugin.SkelPipeFrom
|
||||
for _, f := range s.pipe.From {
|
||||
|
||||
w := &skelpipeFromWrapper{
|
||||
plugin: s.plugin,
|
||||
from: &f,
|
||||
to: &s.pipe.To,
|
||||
}
|
||||
|
||||
if f.SupportPublicKey() {
|
||||
froms = append(froms, &skelpipePublicKeyWrapper{
|
||||
skelpipeFromWrapper: *w,
|
||||
})
|
||||
} else {
|
||||
froms = append(froms, &skelpipePasswordWrapper{
|
||||
skelpipeFromWrapper: *w,
|
||||
})
|
||||
}
|
||||
}
|
||||
return froms
|
||||
}
|
||||
|
||||
func (s *skelpipeToWrapper) User(conn libplugin.ConnMetadata) string {
|
||||
return s.username
|
||||
}
|
||||
|
||||
func (s *skelpipeToWrapper) Host(conn libplugin.ConnMetadata) string {
|
||||
return s.to.Host
|
||||
}
|
||||
|
||||
func (s *skelpipeToWrapper) IgnoreHostKey(conn libplugin.ConnMetadata) bool {
|
||||
return s.to.IgnoreHostkey
|
||||
}
|
||||
|
||||
func (s *skelpipeToWrapper) KnownHosts(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||
return s.plugin.loadFileOrDecodeMany(s.to.KnownHosts, s.to.KnownHostsData, map[string]string{
|
||||
"DOWNSTREAM_USER": conn.User(),
|
||||
"UPSTREAM_USER": s.username,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *skelpipeFromWrapper) MatchConn(conn libplugin.ConnMetadata) (libplugin.SkelPipeTo, error) {
|
||||
user := conn.User()
|
||||
|
||||
matched := s.from.Username == user
|
||||
targetuser := s.to.Username
|
||||
|
||||
if targetuser == "" {
|
||||
targetuser = user
|
||||
}
|
||||
|
||||
if s.from.UsernameRegexMatch {
|
||||
re, err := regexp.Compile(s.from.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matched = re.MatchString(user)
|
||||
|
||||
if matched {
|
||||
targetuser = re.ReplaceAllString(user, s.to.Username)
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
return &skelpipeToWrapper{
|
||||
plugin: s.plugin,
|
||||
username: targetuser,
|
||||
to: s.to,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *skelpipePasswordWrapper) TestPassword(conn libplugin.ConnMetadata, password []byte) (bool, error) {
|
||||
return true, nil // yaml do not test input password
|
||||
}
|
||||
|
||||
func (s *skelpipePublicKeyWrapper) AuthorizedKeys(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||
return s.plugin.loadFileOrDecodeMany(s.from.AuthorizedKeys, s.from.AuthorizedKeysData, map[string]string{
|
||||
"DOWNSTREAM_USER": conn.User(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *skelpipePublicKeyWrapper) TrustedUserCAKeys(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||
return s.plugin.loadFileOrDecodeMany(s.from.TrustedUserCAKeys, s.from.TrustedUserCAKeysData, map[string]string{
|
||||
"DOWNSTREAM_USER": conn.User(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *skelpipeToWrapper) PrivateKey(conn libplugin.ConnMetadata) ([]byte, []byte, error) {
|
||||
p, err := s.plugin.loadFileOrDecode(s.to.PrivateKey, s.to.PrivateKeyData, map[string]string{
|
||||
"DOWNSTREAM_USER": conn.User(),
|
||||
"UPSTREAM_USER": s.username,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return p, nil, nil
|
||||
}
|
||||
|
||||
func (s *skelpipeToWrapper) OverridePassword(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *plugin) listPipe(_ libplugin.ConnMetadata) ([]libplugin.SkelPipe, error) {
|
||||
config, err := p.loadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pipes []libplugin.SkelPipe
|
||||
for _, pipe := range config.Pipes {
|
||||
wrapper := &skelpipeWrapper{
|
||||
plugin: p,
|
||||
pipe: &pipe,
|
||||
}
|
||||
pipes = append(pipes, wrapper)
|
||||
|
||||
}
|
||||
|
||||
return pipes, nil
|
||||
}
|
||||
|
|
@ -4,21 +4,15 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/tg123/sshpiper/libplugin"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type pipeConfigFrom struct {
|
||||
type yamlPipeFrom struct {
|
||||
Username string `yaml:"username"`
|
||||
UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"`
|
||||
AuthorizedKeys listOrString `yaml:"authorized_keys,omitempty"`
|
||||
|
|
@ -27,7 +21,11 @@ type pipeConfigFrom struct {
|
|||
TrustedUserCAKeysData listOrString `yaml:"trusted_user_ca_keys_data,omitempty"`
|
||||
}
|
||||
|
||||
type pipeConfigTo struct {
|
||||
func (f yamlPipeFrom) SupportPublicKey() bool {
|
||||
return f.AuthorizedKeys.Any() || f.AuthorizedKeysData.Any() || f.TrustedUserCAKeys.Any() || f.TrustedUserCAKeysData.Any()
|
||||
}
|
||||
|
||||
type yamlPipeTo struct {
|
||||
Username string `yaml:"username,omitempty"`
|
||||
Host string `yaml:"host"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
|
|
@ -70,27 +68,23 @@ func (l *listOrString) UnmarshalYAML(value *yaml.Node) error {
|
|||
return fmt.Errorf("Failed to unmarshal OneOfType")
|
||||
}
|
||||
|
||||
type pipeConfig struct {
|
||||
From []pipeConfigFrom `yaml:"from,flow"`
|
||||
To pipeConfigTo `yaml:"to,flow"`
|
||||
type yamlPipe struct {
|
||||
From []yamlPipeFrom `yaml:"from,flow"`
|
||||
To yamlPipeTo `yaml:"to,flow"`
|
||||
}
|
||||
|
||||
type piperConfig struct {
|
||||
Version string `yaml:"version"`
|
||||
Pipes []pipeConfig `yaml:"pipes,flow"`
|
||||
Version string `yaml:"version"`
|
||||
Pipes []yamlPipe `yaml:"pipes,flow"`
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
File string
|
||||
NoCheckPerm bool
|
||||
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
func newYamlPlugin() *plugin {
|
||||
return &plugin{
|
||||
cache: cache.New(1*time.Minute, 10*time.Minute),
|
||||
}
|
||||
return &plugin{}
|
||||
}
|
||||
|
||||
func (p *plugin) checkPerm() error {
|
||||
|
|
@ -191,199 +185,3 @@ func (p *plugin) loadFileOrDecodeMany(files listOrString, base64data listOrStrin
|
|||
|
||||
return bytes.Join(byteSlices, []byte("\n")), nil
|
||||
}
|
||||
|
||||
func (p *plugin) supportedMethods() ([]string, error) {
|
||||
config, err := p.loadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
set := make(map[string]bool)
|
||||
|
||||
for _, pipe := range config.Pipes {
|
||||
for _, from := range pipe.From {
|
||||
if from.AuthorizedKeys.Any() || from.AuthorizedKeysData.Any() || from.TrustedUserCAKeys.Any() || from.TrustedUserCAKeysData.Any() {
|
||||
set["publickey"] = true // found authorized_keys, so we support publickey
|
||||
} else {
|
||||
set["password"] = true // no authorized_keys, so we support password
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var methods []string
|
||||
for k := range set {
|
||||
methods = append(methods, k)
|
||||
}
|
||||
return methods, nil
|
||||
}
|
||||
|
||||
func (p *plugin) verifyHostKey(conn libplugin.ConnMetadata, hostname, netaddr string, key []byte) error {
|
||||
item, found := p.cache.Get(conn.UniqueID())
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("connection expired")
|
||||
}
|
||||
|
||||
to := item.(*pipeConfigTo)
|
||||
|
||||
data, err := p.loadFileOrDecodeMany(to.KnownHosts, to.KnownHostsData, map[string]string{
|
||||
"DOWNSTREAM_USER": conn.User(),
|
||||
"UPSTREAM_USER": to.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return libplugin.VerifyHostKeyFromKnownHosts(bytes.NewBuffer(data), hostname, netaddr, key)
|
||||
}
|
||||
|
||||
func (p *plugin) createUpstream(conn libplugin.ConnMetadata, to pipeConfigTo, originPassword string) (*libplugin.Upstream, error) {
|
||||
|
||||
host, port, err := libplugin.SplitHostPortForSSH(to.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &libplugin.Upstream{
|
||||
Host: host,
|
||||
Port: int32(port),
|
||||
UserName: to.Username,
|
||||
IgnoreHostKey: to.IgnoreHostkey,
|
||||
}
|
||||
|
||||
pass := to.Password
|
||||
if pass == "" {
|
||||
pass = originPassword
|
||||
}
|
||||
|
||||
// password found
|
||||
if pass != "" {
|
||||
u.Auth = libplugin.CreatePasswordAuth([]byte(pass))
|
||||
p.cache.Set(conn.UniqueID(), &to, cache.DefaultExpiration)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// try private key
|
||||
data, err := p.loadFileOrDecode(to.PrivateKey, to.PrivateKeyData, map[string]string{
|
||||
"DOWNSTREAM_USER": conn.User(),
|
||||
"UPSTREAM_USER": to.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
u.Auth = libplugin.CreatePrivateKeyAuth(data)
|
||||
p.cache.Set(conn.UniqueID(), &to, cache.DefaultExpiration)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no password or private key found")
|
||||
}
|
||||
|
||||
func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password string, publicKey []byte) (*libplugin.Upstream, error) {
|
||||
user := conn.User()
|
||||
|
||||
config, err := p.loadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var isCert bool
|
||||
var pkcert *ssh.Certificate
|
||||
|
||||
if publicKey != nil {
|
||||
pubKey, err := ssh.ParsePublicKey(publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkcert, isCert = pubKey.(*ssh.Certificate)
|
||||
if isCert {
|
||||
// ensure cert is valid first
|
||||
|
||||
if pkcert.CertType != ssh.UserCert {
|
||||
return nil, fmt.Errorf("only user certificates are supported, cert type: %v", pkcert.CertType)
|
||||
}
|
||||
|
||||
certChecker := ssh.CertChecker{}
|
||||
if err := certChecker.CheckCert(conn.User(), pkcert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, pipe := range config.Pipes {
|
||||
for _, from := range pipe.From {
|
||||
matched := from.Username == user
|
||||
|
||||
if pipe.To.Username == "" {
|
||||
pipe.To.Username = user
|
||||
}
|
||||
|
||||
if from.UsernameRegexMatch {
|
||||
re, err := regexp.Compile(from.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matched = re.MatchString(user)
|
||||
|
||||
if matched {
|
||||
pipe.To.Username = re.ReplaceAllString(user, pipe.To.Username)
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
|
||||
if publicKey == nil && password != "" {
|
||||
return p.createUpstream(conn, pipe.To, password)
|
||||
}
|
||||
|
||||
if isCert {
|
||||
rest, err := p.loadFileOrDecodeMany(from.TrustedUserCAKeys, from.TrustedUserCAKeysData, map[string]string{
|
||||
"DOWNSTREAM_USER": user,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var trustedca ssh.PublicKey
|
||||
for len(rest) > 0 {
|
||||
trustedca, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(trustedca.Marshal(), pkcert.SignatureKey.Marshal()) == 1 {
|
||||
return p.createUpstream(conn, pipe.To, "")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rest, err := p.loadFileOrDecodeMany(from.AuthorizedKeys, from.AuthorizedKeysData, map[string]string{
|
||||
"DOWNSTREAM_USER": user,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var authedPubkey ssh.PublicKey
|
||||
for len(rest) > 0 {
|
||||
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(authedPubkey.Marshal(), publicKey) == 1 {
|
||||
return p.createUpstream(conn, pipe.To, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no matching pipe for username [%v] found", user)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue