finish moving working dir

This commit is contained in:
Boshi Lian 2022-07-04 16:16:47 +00:00
parent fd0a0c628e
commit 19f851fc74
10 changed files with 270 additions and 970 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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"`
}{}
)

View file

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

View file

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

View file

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

View file

@ -1,252 +0,0 @@
// Copyright 2014, 2015 tgic<farmer1992@gmail.com>. 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
}

View file

@ -1,383 +0,0 @@
// Copyright 2014, 2015 tgic<farmer1992@gmail.com>. 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")
}
}