From 19f851fc7462cd9d1e8179e58bfbf9d791ff9343 Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Mon, 4 Jul 2022 16:16:47 +0000 Subject: [PATCH] finish moving working dir --- plugin/internal/workingdir/workingdir.go | 153 +++++++ plugin/workingdir/main.go | 24 +- plugin/workingdirbykey/main.go | 102 +++++ sshpiperd/upstream/workingdir/README.md | 56 --- sshpiperd/upstream/workingdir/config.go | 12 - sshpiperd/upstream/workingdir/pipemgr.go | 80 ---- sshpiperd/upstream/workingdir/plugin.go | 36 -- sshpiperd/upstream/workingdir/v1.go | 142 ------- sshpiperd/upstream/workingdir/workingdir.go | 252 ------------ .../upstream/workingdir/workingdir_test.go | 383 ------------------ 10 files changed, 270 insertions(+), 970 deletions(-) create mode 100644 plugin/internal/workingdir/workingdir.go create mode 100644 plugin/workingdirbykey/main.go delete mode 100644 sshpiperd/upstream/workingdir/README.md delete mode 100644 sshpiperd/upstream/workingdir/config.go delete mode 100644 sshpiperd/upstream/workingdir/pipemgr.go delete mode 100644 sshpiperd/upstream/workingdir/plugin.go delete mode 100644 sshpiperd/upstream/workingdir/v1.go delete mode 100644 sshpiperd/upstream/workingdir/workingdir.go delete mode 100644 sshpiperd/upstream/workingdir/workingdir_test.go diff --git a/plugin/internal/workingdir/workingdir.go b/plugin/internal/workingdir/workingdir.go new file mode 100644 index 00000000..1a68c830 --- /dev/null +++ b/plugin/internal/workingdir/workingdir.go @@ -0,0 +1,153 @@ +package workingdir + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "path" + "regexp" + "strings" + + "github.com/tg123/sshpiper/libplugin" + "golang.org/x/crypto/ssh" + + log "github.com/sirupsen/logrus" +) + +type Workingdir struct { + Path string + NoCheckPerm bool + Strict bool +} + +var ( + usernameRule *regexp.Regexp +) + +const ( + userAuthorizedKeysFile = "authorized_keys" + userKeyFile = "id_rsa" + userUpstreamFile = "sshpiper_upstream" + userKnownHosts = "known_hosts" +) + +func init() { + // Base username validation on Debians default: https://sources.debian.net/src/adduser/3.113%2Bnmu3/adduser.conf/#L85 + // -> NAME_REGEX="^[a-z][-a-z0-9_]*\$" + // The length is limited to 32 characters. See man 8 useradd: https://linux.die.net/man/8/useradd + usernameRule, _ = regexp.Compile("^[a-z_][-a-z0-9_]{0,31}$") +} + +func IsUsernameSecure(user string) bool { + return usernameRule.MatchString(user) +} + +func (w *Workingdir) Mapkey(pub []byte) ([]byte, error) { + + var rest []byte + rest, err := w.readfile(userAuthorizedKeysFile) + 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 bytes.Equal(authedPubkey.Marshal(), pub) { + log.Infof("found mapping key %v", w.fullpath(userKeyFile)) + return w.readfile(userKeyFile) + } + } + + return nil, fmt.Errorf("no matching key found") +} + +func (w *Workingdir) CreateUpstream() (*libplugin.Upstream, error) { + + data, err := w.readfile(userUpstreamFile) + if err != nil { + return nil, err + } + + host, port, user, err := parseUpstreamFile(string(data)) + if err != nil { + return nil, err + } + + return &libplugin.Upstream{ + Host: host, + Port: int32(port), + UserName: user, + IgnoreHostKey: !w.Strict, + }, nil +} + +func (w *Workingdir) checkPerm(file string) error { + filename := path.Join(w.Path, file) + f, err := os.Open(filename) + if err != nil { + return err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return err + } + + if w.NoCheckPerm { + return nil + } + + if fi.Mode().Perm()&0077 != 0 { + return fmt.Errorf("%v's perm is too open", filename) + } + + return nil +} + +func (w *Workingdir) fullpath(file string) string { + return path.Join(w.Path, file) +} + +func (w *Workingdir) readfile(file string) ([]byte, error) { + if err := w.checkPerm(file); err != nil { + return nil, err + } + + return ioutil.ReadFile(w.fullpath(file)) +} + +func parseUpstreamFile(data string) (host string, port int, user string, err error) { + r := bufio.NewReader(strings.NewReader(data)) + for { + host, err = r.ReadString('\n') + if err != nil { + break + } + + host = strings.TrimSpace(host) + + if host != "" && host[0] != '#' { + break + } + } + + t := strings.SplitN(host, "@", 2) + + if len(t) > 1 { + user = t[0] + host = t[1] + } + + host, port, err = libplugin.SplitHostPortForSSH(host) + return +} diff --git a/plugin/workingdir/main.go b/plugin/workingdir/main.go index f44bff9e..83844e04 100644 --- a/plugin/workingdir/main.go +++ b/plugin/workingdir/main.go @@ -6,21 +6,23 @@ import ( "github.com/tg123/sshpiper/libplugin" "github.com/urfave/cli/v2" + + "github.com/tg123/sshpiper/plugin/internal/workingdir" ) -func createWorkingdir(c *cli.Context, user string) (*workingdir, error) { +func createWorkingdir(c *cli.Context, user string) (*workingdir.Workingdir, error) { if !c.Bool("allow-baduser-name") { - if !isUsernameSecure(user) { + if !workingdir.IsUsernameSecure(user) { return nil, fmt.Errorf("bad username: %s", user) } } root := c.String("root") - return &workingdir{ - path: path.Join(root, user), - noCheckPerm: c.Bool("no-check-perm"), - strict: c.Bool("strict-hostkey"), + return &workingdir.Workingdir{ + Path: path.Join(root, user), + NoCheckPerm: c.Bool("no-check-perm"), + Strict: c.Bool("strict-hostkey"), }, nil } @@ -66,7 +68,7 @@ func main() { return nil, err } - u, err := w.createUpstream() + u, err := w.CreateUpstream() if err != nil { return nil, err } @@ -81,12 +83,16 @@ func main() { return nil, err } - u, err := w.createUpstream() + u, err := w.CreateUpstream() + if err != nil { + return nil, err + } + + k, err := w.Mapkey(key) if err != nil { return nil, err } - k, err := w.mapkey(key) u.Auth = libplugin.CreatePrivateKeyAuth(k) return u, nil diff --git a/plugin/workingdirbykey/main.go b/plugin/workingdirbykey/main.go new file mode 100644 index 00000000..b6523894 --- /dev/null +++ b/plugin/workingdirbykey/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "os" + "path" + "path/filepath" + + "github.com/tg123/sshpiper/libplugin" + "github.com/urfave/cli/v2" + + "github.com/tg123/sshpiper/plugin/internal/workingdir" + + log "github.com/sirupsen/logrus" +) + +func main() { + + libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{ + Name: "workingdir", + Usage: "sshpiperd workingdir plugin", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "root", + Usage: "path to root working directory", + Value: "/var/sshpiper", + EnvVars: []string{"SSHPIPERD_WORKINGDIR_ROOT"}, + }, + &cli.BoolFlag{ + Name: "allow-baduser-name", + Usage: "allow bad username", + EnvVars: []string{"SSHPIPERD_WORKINGDIR_ALLOWBADUSERNAME"}, + }, + &cli.BoolFlag{ + Name: "no-check-perm", + Usage: "disable 0400 checking", + EnvVars: []string{"SSHPIPERD_WORKINGDIR_NOCHECKPERM"}, + }, + &cli.BoolFlag{ + Name: "strict-hostkey", + Usage: "upstream host public key must be in known_hosts file, otherwise drop the connection", + EnvVars: []string{"SSHPIPERD_WORKINGDIR_STRICTHOSTKEY"}, + }, + }, + CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) { + + root := c.String("root") + + return &libplugin.SshPiperPluginConfig{ + PublicKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (*libplugin.Upstream, error) { + + userdir := path.Join(root, conn.User()) + + var upstream *libplugin.Upstream + + filepath.Walk(userdir, func(path string, info os.FileInfo, err error) error { + + log.Infof("search public key in path: %v", path) + if err != nil { + log.Infof("error walking path: ", err) + return nil + } + + if !info.IsDir() { + return nil + } + + w := &workingdir.Workingdir{ + Path: path, + NoCheckPerm: c.Bool("no-check-perm"), + Strict: c.Bool("strict-hostkey"), + } + + u, err := w.CreateUpstream() + if err != nil { + return nil + } + + k, err := w.Mapkey(key) + if err != nil { + return nil + } + + u.Auth = libplugin.CreatePrivateKeyAuth(k) + upstream = u + return fmt.Errorf("stop") + }) + + if upstream != nil { + return upstream, nil + } + + return nil, fmt.Errorf("no matching public key found in %v", userdir) + }, + + VerifyHostKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (bool, error) { + return true, nil + }, + }, nil + }, + }) +} diff --git a/sshpiperd/upstream/workingdir/README.md b/sshpiperd/upstream/workingdir/README.md deleted file mode 100644 index 475671c0..00000000 --- a/sshpiperd/upstream/workingdir/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Working Directory for SSHPiper - -`Working Dir` is a `/home`-like directory. -SSHPiperd read files from `workingdir/[username]/` to know upstream's configuration. - -e.g. - -``` -workingdir tree - -. -├── github -│   └── sshpiper_upstream -└── linode - └── sshpiper_upstream -``` - -when `ssh sshpiper_host -l github`, -sshpiper reads `workingdir/github/sshpiper_upstream` and the connect to the upstream. - -## User files - -*These file MUST NOT be accessible to group or other. (chmod og-rwx filename)* - - * sshpiper_upstream - - * line starts with `#` are treated as comment - * only the first not comment line will be parsed - * if no port was given, 22 will be used as default - * if `user@` was defined, username to upstream will be the mapped one - -``` -# comment -[user@]upstream[:22] -``` - -``` -e.g. - -git@github.com - -google.com:12345 - -``` - - * authorized_keys - - OpenSSH format `authorized_keys` (see `~/.ssh/authorized_keys`). Used for `publickey sign again(see below)`. - - * id_rsa - - RSA key for `publickey sign again(see below)`. - - * known_hosts - - when `upstream-workingdir-stricthostkey` is set, upstream server's public key must present in known_hosts diff --git a/sshpiperd/upstream/workingdir/config.go b/sshpiperd/upstream/workingdir/config.go deleted file mode 100644 index 4ff12a0a..00000000 --- a/sshpiperd/upstream/workingdir/config.go +++ /dev/null @@ -1,12 +0,0 @@ -package workingdir - -var ( - config = struct { - WorkingDir string `long:"upstream-workingdir" default:"/var/sshpiper" description:"Path to working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR" ini-name:"upstream-workingdir"` - AllowBadUsername bool `long:"upstream-workingdir-allowbadusername" description:"Disable username check while search the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_ALLOWBADUSERNAME" ini-name:"upstream-workingdir-allowbadusername"` - NoCheckPerm bool `long:"upstream-workingdir-nocheckperm" description:"Disable 0400 checking when using files in the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_NOCHECKPERM" ini-name:"upstream-workingdir-nocheckperm"` - FallbackUsername string `long:"upstream-workingdir-fallbackusername" description:"Fallback to a user when user does not exists in directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_FALLBACKUSERNAME" ini-name:"upstream-workingdir-fallbackusername"` - StrictHostKey bool `long:"upstream-workingdir-stricthostkey" description:"Upstream host public key must be in known_hosts file, otherwise drop the connection" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_STRICTHOSTKEY" ini-name:"upstream-workingdir-stricthostkey"` - MatchPublicKeyInSubDir bool `long:"upstream-workingdir-matchpublickeyinsubdir" description:"Remap user in user's sub dir with publickey" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_MATCHPUBLICKEYINSUBDIR" ini-name:"upstream-workingdir-matchpublickeyinsubdir"` - }{} -) diff --git a/sshpiperd/upstream/workingdir/pipemgr.go b/sshpiperd/upstream/workingdir/pipemgr.go deleted file mode 100644 index 3361554e..00000000 --- a/sshpiperd/upstream/workingdir/pipemgr.go +++ /dev/null @@ -1,80 +0,0 @@ -package workingdir - -import ( - "fmt" - "io/ioutil" - "os" - "path" - - "github.com/tg123/sshpiper/sshpiperd/upstream" -) - -func (p *plugin) ListPipe() ([]upstream.Pipe, error) { - files, err := ioutil.ReadDir(config.WorkingDir) - if err != nil { - return nil, err - } - - pipes := make([]upstream.Pipe, 0, len(files)) - for _, file := range files { - if !file.IsDir() { - continue - } - - userUpstreamFile := userFile{filename: userUpstreamFile, userdir: path.Join(config.WorkingDir, file.Name())} - data, err := userUpstreamFile.read() - if err != nil { - continue - } - - host, port, mappedUser, err := parseUpstreamFile(string(data)) - if err != nil { - continue - } - - pipes = append(pipes, upstream.Pipe{ - Host: host, - Port: port, - Username: file.Name(), - UpstreamUsername: mappedUser, - }) - } - - return pipes, nil -} - -func (p *plugin) CreatePipe(opt upstream.CreatePipeOption) error { - userdir := path.Join(config.WorkingDir, opt.Username) - err := os.MkdirAll(userdir, 0775) - if err != nil { - return err - } - - userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir} - path := userUpstreamFile.realPath() - if _, err := os.Stat(path); os.IsNotExist(err) { - - upuser := opt.UpstreamUsername - - if len(upuser) == 0 { - upuser = opt.Username - } - - content := fmt.Sprintf("%v@%v:%v", upuser, opt.Host, opt.Port) - return ioutil.WriteFile(path, []byte(content), 0600) - } else if err != nil { - return err - } - - return fmt.Errorf("upstream file of [%v] alreay exists", opt.Username) -} - -func (p *plugin) RemovePipe(name string) error { - userUpstreamFile := userFile{filename: userUpstreamFile, userdir: path.Join(config.WorkingDir, name)} - path := userUpstreamFile.realPath() - if _, err := os.Stat(path); os.IsNotExist(err) { - return nil - } - - return os.Remove(path) -} diff --git a/sshpiperd/upstream/workingdir/plugin.go b/sshpiperd/upstream/workingdir/plugin.go deleted file mode 100644 index 8289c278..00000000 --- a/sshpiperd/upstream/workingdir/plugin.go +++ /dev/null @@ -1,36 +0,0 @@ -package workingdir - -import ( - log "github.com/sirupsen/logrus" - "github.com/tg123/sshpiper/sshpiperd/upstream" -) - -var logger *log.Logger - -type plugin struct { -} - -func (p *plugin) GetName() string { - return "workingdir" -} - -func (p *plugin) GetOpts() interface{} { - return &config -} - -func (p *plugin) GetHandler() upstream.Handler { - return findUpstreamFromUserfile -} - -func (p *plugin) Init(glogger *log.Logger) error { - - logger = glogger - - logger.Printf("upstream provider: workingdir from path [%v] initializing", config.WorkingDir) - - return nil -} - -func init() { - upstream.Register("workingdir", &plugin{}) -} diff --git a/sshpiperd/upstream/workingdir/v1.go b/sshpiperd/upstream/workingdir/v1.go deleted file mode 100644 index d2430167..00000000 --- a/sshpiperd/upstream/workingdir/v1.go +++ /dev/null @@ -1,142 +0,0 @@ -package workingdir - -import ( - "fmt" - "net" - "os" - "path" - "path/filepath" - - "github.com/tg123/sshpiper/sshpiperd/v0bridge" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/knownhosts" -) - -func (p *plugin) InstallUpstream(piper *ssh.PiperConfig) error { - v0bridge.InstallUpstream(piper, p.GetHandler()) - - piper.PublicKeyCallback = p.matchPublicKeyInSubDir - - if config.MatchPublicKeyInSubDir { - old := piper.NextAuthMethods - piper.NextAuthMethods = func(conn ssh.ConnMetadata, ctx ssh.ChallengeContext) ([]string, error) { - methods, err := old(conn, ctx) - methods = append(methods, "publickey") - return methods, err - } - } - - return nil -} - -func (p *plugin) matchPublicKeyDir(conn ssh.ConnMetadata, key ssh.PublicKey, user, userdir string) (*ssh.Upstream, error) { - if !checkUsername(user) { - return nil, fmt.Errorf("downstream is not using a valid username") - } - - userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir} - err := userUpstreamFile.checkPerm() - - if os.IsNotExist(err) && len(config.FallbackUsername) > 0 { - user = config.FallbackUsername - } else if err != nil { - return nil, err - } - - data, err := userUpstreamFile.read() - if err != nil { - return nil, err - } - - host, port, mappedUser, err := parseUpstreamFile(string(data)) - if err != nil { - return nil, err - } - addr := fmt.Sprintf("%v:%v", host, port) - - logger.Printf("mapping user [%v] to [%v@%v]", user, mappedUser, addr) - - c, err := net.Dial("tcp", addr) - if err != nil { - return nil, err - } - - hostKeyCallback := ssh.InsecureIgnoreHostKey() - - if config.StrictHostKey { - userKnownHosts := userFile{filename: userKnownHosts, userdir: userdir} - hostKeyCallback, err = knownhosts.New(userKnownHosts.realPath()) - - if err != nil { - return nil, err - } - } - - signer, err := mapPublicKeyFromUserfile(conn, user, userdir, key) - if err != nil { - return nil, err - } - - if signer == nil { - return nil, fmt.Errorf("cant find public key in user folder") - } - - return &ssh.Upstream{ - Conn: c, - ClientConfig: ssh.ClientConfig{ - User: mappedUser, - Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, - HostKeyCallback: hostKeyCallback, - }, - }, nil -} - -func (p *plugin) matchPublicKeyInSubDir(conn ssh.ConnMetadata, key ssh.PublicKey, _ ssh.ChallengeContext) (*ssh.Upstream, error) { - - { - userdir := path.Join(config.WorkingDir, conn.User()) - u, err := p.matchPublicKeyDir(conn, key, conn.User(), userdir) - if err != nil && !config.MatchPublicKeyInSubDir { - logger.Errorf("cannot map private key in %v: %v", userdir, err) - return nil, err - } - - if u != nil { - return u, nil - } - } - - var upstream *ssh.Upstream - - // search in working dir - filepath.Walk(config.WorkingDir, func(path string, info os.FileInfo, err error) error { - - logger.Debugf("search public key in path: %v", path) - if err != nil { - logger.Debug("error walking path: ", err) - return nil - } - - if !info.IsDir() { - return nil - } - - u, err := p.matchPublicKeyDir(conn, key, conn.User(), path) - if err != nil { - logger.Infof("cannot map private key in %v: %v, search next", path, err) - } - - if u != nil { - upstream = u - return fmt.Errorf("stop") - } - - return nil - }) - - if upstream != nil { - return upstream, nil - } - - return nil, fmt.Errorf("no matching public key found in %v", config.WorkingDir) -} diff --git a/sshpiperd/upstream/workingdir/workingdir.go b/sshpiperd/upstream/workingdir/workingdir.go deleted file mode 100644 index 674e03d3..00000000 --- a/sshpiperd/upstream/workingdir/workingdir.go +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2014, 2015 tgic. All rights reserved. -// this file is governed by MIT-license -// -// https://github.com/tg123/sshpiper - -package workingdir - -import ( - "bufio" - "bytes" - "fmt" - "io/ioutil" - "net" - "os" - "path" - "regexp" - "strings" - - "github.com/tg123/sshpiper/sshpiperd/upstream" - "github.com/tg123/sshpiper/sshpiperd/v0bridge" - - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/knownhosts" -) - -type userFile struct { - filename string - userdir string -} - -const ( - userAuthorizedKeysFile = "authorized_keys" - userKeyFile = "id_rsa" - userUpstreamFile = "sshpiper_upstream" - userKnownHosts = "known_hosts" -) - -var ( - usernameRule *regexp.Regexp -) - -func init() { - // Base username validation on Debians default: https://sources.debian.net/src/adduser/3.113%2Bnmu3/adduser.conf/#L85 - // -> NAME_REGEX="^[a-z][-a-z0-9_]*\$" - // The length is limited to 32 characters. See man 8 useradd: https://linux.die.net/man/8/useradd - usernameRule, _ = regexp.Compile("^[a-z_][-a-z0-9_]{0,31}$") -} - -func (file userFile) userSpecFile(filename string) string { - p := file.userdir - if p == "" { - p = config.WorkingDir - } - return path.Join(p, filename) -} - -func (file userFile) read() ([]byte, error) { - return ioutil.ReadFile(file.userSpecFile(file.filename)) -} - -func (file userFile) realPath() string { - return file.userSpecFile(file.filename) -} - -// return error if other and group have access right -func (file userFile) checkPerm() error { - filename := file.userSpecFile(file.filename) - f, err := os.Open(filename) - if err != nil { - return err - } - defer f.Close() - - fi, err := f.Stat() - if err != nil { - return err - } - - if config.NoCheckPerm { - return nil - } - - if fi.Mode().Perm()&0077 != 0 { - return fmt.Errorf("%v's perm is too open", filename) - } - - return nil -} - -// return false if username is not a valid unix user name -// this is for security reason -func checkUsername(user string) bool { - if config.AllowBadUsername { - return true - } - - return usernameRule.MatchString(user) -} - -func parseUpstreamFile(data string) (host string, port int, user string, err error) { - r := bufio.NewReader(strings.NewReader(data)) - for { - host, err = r.ReadString('\n') - if err != nil { - break - } - - host = strings.TrimSpace(host) - - if host != "" && host[0] != '#' { - break - } - } - - t := strings.SplitN(host, "@", 2) - - if len(t) > 1 { - user = t[0] - host = t[1] - } - - host, port, err = upstream.SplitHostPortForSSH(host) - return -} - -func findUpstreamFromUserfile(conn ssh.ConnMetadata, _ ssh.ChallengeContext) (net.Conn, *v0bridge.AuthPipe, error) { - user := conn.User() - userdir := path.Join(config.WorkingDir, conn.User()) - - if !checkUsername(user) { - return nil, nil, fmt.Errorf("downstream is not using a valid username") - } - - userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir} - err := userUpstreamFile.checkPerm() - - if os.IsNotExist(err) && len(config.FallbackUsername) > 0 { - user = config.FallbackUsername - } else if err != nil { - return nil, nil, err - } - - data, err := userUpstreamFile.read() - if err != nil { - return nil, nil, err - } - - host, port, mappedUser, err := parseUpstreamFile(string(data)) - if err != nil { - return nil, nil, err - } - addr := fmt.Sprintf("%v:%v", host, port) - - logger.Printf("mapping user [%v] to [%v@%v]", user, mappedUser, addr) - - c, err := net.Dial("tcp", addr) - if err != nil { - return nil, nil, err - } - - hostKeyCallback := ssh.InsecureIgnoreHostKey() - - if config.StrictHostKey { - userKnownHosts := userFile{filename: userKnownHosts, userdir: userdir} - hostKeyCallback, err = knownhosts.New(userKnownHosts.realPath()) - - if err != nil { - return nil, nil, err - } - } - - return c, &v0bridge.AuthPipe{ - User: mappedUser, - - PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (v0bridge.AuthPipeType, ssh.AuthMethod, error) { - signer, err := mapPublicKeyFromUserfile(conn, user, userdir, key) - - if err != nil || signer == nil { - // try one - return v0bridge.AuthPipeTypeNone, nil, nil - } - - return v0bridge.AuthPipeTypeMap, ssh.PublicKeys(signer), nil - }, - - UpstreamHostKeyCallback: hostKeyCallback, - }, nil -} - -func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, user, userdir string, key ssh.PublicKey) (signer ssh.Signer, err error) { - defer func() { // print error when func exit - if err != nil { - logger.Printf("mapping private key error: %v, public key auth denied for [%v] from [%v]", err, user, conn.RemoteAddr()) - } - }() - - userAuthorizedKeysFile := userFile{filename: userAuthorizedKeysFile, userdir: userdir} - err = userAuthorizedKeysFile.checkPerm() - - if os.IsNotExist(err) && len(config.FallbackUsername) > 0 { - err = nil - user = config.FallbackUsername - } else if err != nil { - return nil, err - } - - keydata := key.Marshal() - - var rest []byte - rest, err = userAuthorizedKeysFile.read() - if err != nil { - return nil, err - } - - userKeyFile := userFile{filename: userKeyFile, userdir: userdir} - var authedPubkey ssh.PublicKey - - for len(rest) > 0 { - authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest) - - if err != nil { - return nil, err - } - - if bytes.Equal(authedPubkey.Marshal(), keydata) { - err = userKeyFile.checkPerm() - if err != nil { - return nil, err - } - - var privateBytes []byte - privateBytes, err = userKeyFile.read() - if err != nil { - return nil, err - } - - var private ssh.Signer - private, err = ssh.ParsePrivateKey(privateBytes) - if err != nil { - return nil, err - } - - // in log may see this twice, one is for query the other is real sign again - logger.Printf("auth succ, using mapped private key [%v] for user [%v] from [%v]", userKeyFile.realPath(), user, conn.RemoteAddr()) - return private, nil - } - } - - logger.Printf("public key auth failed user [%v] from [%v]", conn.User(), conn.RemoteAddr()) - - return nil, nil -} diff --git a/sshpiperd/upstream/workingdir/workingdir_test.go b/sshpiperd/upstream/workingdir/workingdir_test.go deleted file mode 100644 index a08eef05..00000000 --- a/sshpiperd/upstream/workingdir/workingdir_test.go +++ /dev/null @@ -1,383 +0,0 @@ -// Copyright 2014, 2015 tgic. All rights reserved. -// this file is governed by MIT-license -// -// https://github.com/tg123/sshpiper - -package workingdir - -import ( - "bytes" - "io" - "io/ioutil" - - log "github.com/sirupsen/logrus" - - "net" - "os" - "testing" - - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/testdata" -) - -func init() { - logger = log.New() -} - -func buildWorkingDir(users []string, t *testing.T) { - config.WorkingDir = "" - dir, err := ioutil.TempDir(os.TempDir(), "sshpiperd_workingdir") - - if err != nil { - t.Fatalf("setup temp dir:%v", err) - } - - config.WorkingDir = dir - - for _, u := range users { - if err := os.Mkdir(config.WorkingDir+"/"+u, os.ModePerm); err != nil { - t.Fatalf("mkdir dir:%v", err) - } - } - - t.Logf("switch workingdir to %v", config.WorkingDir) -} - -func cleanupWorkdir(t *testing.T) { - if config.WorkingDir == "" { - return - } - - t.Logf("cleaning workingdir %v", config.WorkingDir) - - os.RemoveAll(config.WorkingDir) -} - -func TestReadUserFile(t *testing.T) { - user1 := "testuser1" - user2 := "testuser2" - - buildWorkingDir([]string{user1, user2}, t) - defer cleanupWorkdir(t) - - data1 := []byte("byte[] := data1") - data2 := []byte("this is data2") - - f := userFile("f") - - err := ioutil.WriteFile(f.realPath(user1), data1, os.ModePerm) - if err != nil { - t.Fatalf("cant create file: %v", err) - } - - err = ioutil.WriteFile(f.realPath(user2), data2, os.ModePerm) - if err != nil { - t.Fatalf("cant create file: %v", err) - } - - d, err := f.read(user1) - if err != nil || !bytes.Equal(d, data1) { - t.Fatalf("read faild") - } - - d, err = f.read(user2) - if err != nil || bytes.Equal(d, data1) { - t.Fatalf("reading wrong user file") - } -} - -func TestCheckPerm(t *testing.T) { - user := "testuser" - buildWorkingDir([]string{user}, t) - defer cleanupWorkdir(t) - - f := userFile("perm") - - err := ioutil.WriteFile(f.realPath(user), nil, os.ModePerm) - if err != nil { - t.Fatalf("cant create file: %v", err) - } - - err = f.checkPerm(user) - if err == nil { - t.Fatalf("should fail when read 0777 user file") - } - - err = os.Chmod(f.realPath(user), 0600) - if err != nil { - t.Fatalf("cant change file mode %v", err) - } - - err = f.checkPerm(user) - if err != nil { - t.Fatalf("fail when read 0600 user file %v", err) - } -} - -type stubConnMetadata struct{ user string } - -func (s stubConnMetadata) User() string { - return s.user -} - -func (s stubConnMetadata) SessionID() []byte { return nil } -func (s stubConnMetadata) ClientVersion() []byte { return nil } -func (s stubConnMetadata) ServerVersion() []byte { return nil } -func (s stubConnMetadata) RemoteAddr() net.Addr { return nil } -func (s stubConnMetadata) LocalAddr() net.Addr { return nil } - -func TestParseUpstreamFile(t *testing.T) { - - { - - addr, port, user, err := parseUpstreamFile(` - -a:123 - -`) - if err != nil { - t.Fatalf("should not return err: %v", err) - } - - if addr != "a" || port != 123 || user != "" { - t.Fatalf("parse failed common with port") - } - } - - { - - addr, port, user, err := parseUpstreamFile(` -a:123 -b:456 -`) - - if err != nil { - t.Fatalf("should not return err: %v", err) - } - - if addr != "a" || port != 123 || user != "" { - t.Fatalf("parse multi line") - } - } - - { - - addr, port, user, err := parseUpstreamFile(` -host -`) - if err != nil { - t.Fatalf("should not return err: %v", err) - } - - if addr != "host" || port != 22 || user != "" { - t.Fatalf("parse no port") - } - } - - { - - addr, port, user, err := parseUpstreamFile(` -user@github.com -`) - if err != nil { - t.Fatalf("should not return err: %v", err) - } - - if addr != "github.com" || port != 22 || user != "user" { - t.Fatalf("parse no port with user") - } - } - - { - - _, _, _, err := parseUpstreamFile(``) - - if err == nil { - t.Fatalf("empty file") - } - } - - { - - addr, port, user, err := parseUpstreamFile(` - -# comment -user@github.com -test@linode.com -`) - - if err != nil { - t.Fatalf("should not return err: %v", err) - } - if addr != "github.com" || port != 22 || user != "user" { - t.Fatalf("multi line with comment") - } - } -} - -func TestFindUpstreamFromUserfile(t *testing.T) { - user := "testuser" - buildWorkingDir([]string{user}, t) - defer cleanupWorkdir(t) - - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("cant create fake server: %v", err) - } - defer listener.Close() - - go func() { - c, err := listener.Accept() - if err != nil { - t.Errorf("fake server error %v", err) - return - } - defer c.Close() - if _, err := io.Copy(c, c); err != nil { - t.Errorf("fake server copy error %v", err) - return - } - }() - - addr := listener.Addr().String() - t.Logf("fake server at %v", addr) - - err = ioutil.WriteFile(userUpstreamFile.realPath(user), []byte(addr), 0777) - if err != nil { - t.Fatalf("cant create file: %v", err) - } - - t.Logf("testing file too open") - _, _, err = findUpstreamFromUserfile(stubConnMetadata{user}, nil) - if err == nil { - t.Fatalf("should return err when file too open") - } - - err = os.Chmod(userUpstreamFile.realPath(user), 0400) - if err != nil { - t.Fatalf("cant change file mode %v", err) - } - - t.Logf("testing conn dial to %v", addr) - conn, _, err := findUpstreamFromUserfile(stubConnMetadata{user}, nil) - if err != nil { - t.Fatalf("findUpstreamFromUserfile failed %v", err) - } - defer conn.Close() - - d := []byte("hello") - - _, err = conn.Write(d) - if err != nil { - t.Fatalf("cant write to conn: %v", err) - } - - b := make([]byte, len(d)) - _, err = conn.Read(b) - - if err != nil || !bytes.Equal(b, d) { - t.Fatalf("conn to upstream does not work") - } - - t.Logf("testing user not found") - config.FallbackUsername = "" - _, _, err = findUpstreamFromUserfile(stubConnMetadata{"nosuchuser"}, nil) - if err == nil { - t.Fatalf("should return err when finding nosuchuser") - } - - t.Logf("testing user not found fallback") - config.FallbackUsername = user - _, _, err = findUpstreamFromUserfile(stubConnMetadata{"nosuchuser"}, nil) - - if err != nil { - t.Fatalf("should return fallbackuser") - } -} - -func TestMapPublicKeyFromUserfile(t *testing.T) { - user := "testuser" - buildWorkingDir([]string{user}, t) - defer cleanupWorkdir(t) - - privateKey, _ := ssh.ParsePrivateKey(testdata.PEMBytes["rsa"]) - publicKey := privateKey.PublicKey() - privateKey2, _ := ssh.ParsePrivateKey(testdata.PEMBytes["dsa"]) - - _ = privateKey2 - - err := ioutil.WriteFile(userKeyFile.realPath(user), testdata.PEMBytes["rsa"], 0777) - if err != nil { - t.Fatalf("cant create file: %v", err) - } - - authKeys := ssh.MarshalAuthorizedKey(publicKey) - err = ioutil.WriteFile(userAuthorizedKeysFile.realPath(user), authKeys, 0777) - if err != nil { - t.Fatalf("cant create file: %v", err) - } - - t.Logf("testing file too open") - - // UserAuthorizedKeysFile - _, err = mapPublicKeyFromUserfile(stubConnMetadata{user}, publicKey) - if err == nil { - t.Fatalf("should return err when file too open") - } - - err = os.Chmod(userAuthorizedKeysFile.realPath(user), 0600) - if err != nil { - t.Fatalf("cant change file mode %v", err) - } - - // UserKeyFile - _, err = mapPublicKeyFromUserfile(stubConnMetadata{user}, publicKey) - if err == nil { - t.Fatalf("should return err when file too open") - } - - err = os.Chmod(userKeyFile.realPath(user), 0600) - if err != nil { - t.Fatalf("cant change file mode %v", err) - } - - t.Logf("testing user not found") - config.FallbackUsername = "" - _, err = mapPublicKeyFromUserfile(stubConnMetadata{"nosuchuser"}, publicKey) - if err == nil { - t.Fatalf("should return err when mapping from nosuchuser") - } - - t.Logf("testing user not found fallback") - config.FallbackUsername = user - _, err = mapPublicKeyFromUserfile(stubConnMetadata{"nosuchuser"}, publicKey) - if err != nil { - t.Fatalf("should return fallbackuser") - } - - t.Logf("testing mapping signer") - signer, err := mapPublicKeyFromUserfile(stubConnMetadata{user}, privateKey.PublicKey()) - if err != nil { - t.Fatalf("error mapping key %v", err) - } - - if !bytes.Equal(signer.PublicKey().Marshal(), privateKey.PublicKey().Marshal()) { - t.Fatalf("id_rsa not the same") - } - - t.Logf("testing not in UserAuthorizedKeysFile") - - authKeys = ssh.MarshalAuthorizedKey(privateKey2.PublicKey()) - err = ioutil.WriteFile(userAuthorizedKeysFile.realPath(user), authKeys, 0600) - if err != nil { - t.Fatalf("cant create file: %v", err) - } - - signer, err = mapPublicKeyFromUserfile(stubConnMetadata{user}, privateKey.PublicKey()) - if err != nil { - t.Fatalf("cant mapping key: %v", err) - } - if signer != nil { - t.Fatalf("should not map private key when public key not in UserAuthorizedKeysFile") - } -}