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:
Boshi Lian 2024-10-24 23:15:26 -07:00 • committed by GitHub
parent adc5d51c33
commit 14ceadb8e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1200 additions and 819 deletions

View file

@ -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
View 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
}

View file

@ -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)
}