package sftp // sftp server integration tests// enable with -integration// example invokation (darwin): gofmt -w `find . -name \*.go` && (cd server_standalone/ ; go build -tags debug) && go test -tags debug github.com/pkg/sftp -integration -v -sftp /usr/libexec/sftp-server -run ServerCompareSubsystems import ( "bytes" "crypto/ecdsa" "crypto/elliptic" crand "crypto/rand" "crypto/x509" "encoding/hex" "encoding/pem" "flag" "fmt" "io/ioutil" "math/rand" "net" "os" "os/exec" "path" "path/filepath" "regexp" "strconv" "strings" "testing" "time" "github.com/kr/fs" "golang.org/x/crypto/ssh") var testSftpClientBin = flag.String("sftp_client", "/usr/bin/sftp", "location of the sftp client binary")var sshServerDebugStream = ioutil.Discardvar sftpServerDebugStream = ioutil.Discardvar sftpClientDebugStream = ioutil.Discard const ( GOLANG_SFTP = true OPENSSH_SFTP = false) var ( hostPrivateKeySigner ssh.Signer privKey = []byte(`-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEArhp7SqFnXVZAgWREL9Ogs+miy4IU/m0vmdkoK6M97G9NX/Pjwf8I/3/ynxmcArbt8Rc4JgkjT2uxx/NqR0yN42N1PjO5Czu0dms1PSqcKIJdeUBV7gdrKSm9Co4d2vwfQp5mg47eG4w63pz7Drk9+VIyi9YiYH4bve7WnGDswn4ycvYZslV5kKnjlfCdPig+g5P7yQYud0cDWVwyA0+kxvL6H3Ip+Fu8rLDZn4/P1WlFAIucPAf4uEKDGGmC2URowi5eesYR7f6GN/HnBs2776laNlAVXZUmYTUfOGagwLsEkx8xXdNqntfbs2MOOoK+myJrNtcB9pCrM0H6um19uQIDAQABAoIBABkWr9WdVKvalgkPTdQmhu3mKRNyd1wCl+1voZ5IM9Ayac/98UAvZDiNU4Uhx52MhtVLJ0gz4Oa8+i16IkKMAZZW6ro/8dZwkBzQbieWUFJ2Fso2PyvB3etcnGU8/Yhk9IxBDzy+BbuqhYE21ebVQtz+v1HvVZzaD11bYYm/Xd7Y28QREVfFen30Q/v3dv7dOteDE/RgDS8Czz7wjMW32Q8JL5grz7zPkMK39BLXsTcSYcaasT2ParROhGJZDmbgd3l33zKCVc1zcj9BSA47QljGd09Tys958WWHgtj2o7bp9v1Ufs4LnyKgzrB80WX1ovaSQKvd5THTLchOkLIhUAECgYEA2doGXy9wMBmTn/hjiVvggR1aKiBwUpnB87Hn5xCMgoECVhFZlT6lWmZe7R2klbtG1aYlw+y+uzHhoVDAJW9AUSV8qoDUwbRXvBVlp+In5wIqJ+VjfivKzgIfzomL5NvDz37cvPmzqIeySTowEfbQyq7CUQSoDtE9H97E2wWZhDkCgYEAzJdJk+NSFoTkHhfD3L0xCDHpRV3gvaOeew8524fVtVUq53X8m91ng4AX1r74dCUYwwiFgqTtSSJfx2iH1xKnNq28M9uKg7wOrCKrRqNPnYUO3LehZEC7rwUr26z4iJDHjjoBuBcS7nw0LJ+0Zeg1IF+aIdZGV3MrAKnrzWPixYECgYBsffX6ZWebrMEmQ89eUtFFu9ZxcGI/4K8ErC7vlgBD5ffB4TYZ627xzFWuBLs4jmHCeNIJ9tct5rOVYN+wRO1k/CRPzYUnSqb+1jEgILL6istvvv+DkE+ZtNkeRMXUndWwel94BWsBnUKe0UmrSJ3Gsq23J3iCmJW2T3z+DpXbkQKBgQCK+LUVDNPE0i42NsRnm+fDfkvLP7Kafpr3UmdltMY474o+QYn+wg0/aPJIf9463rwMNyyhirBX/k57IIktUdFdtfPicd2MEGETElWvnN1GzYxD50Rs2f/jKisZhEwqT9YNyV9DkgDdGGdEbJNYqbv0qpwDIg8T9foe8E1pbdErgQKBgAt290I3L316cdxIQTkJh1DlScN/unFffITwu127WMr28Jt3mq3cZpuMAecey/eEKCj+Rlas5NDYKsB18QIuAw+qqWyq0LAKLiAvP1965Rkc4PLScl3MgJtOQYa37FK0p8NcDeUuF86zXBVutwS5nJLchHhKfd590ks57OROtm29-----END RSA PRIVATE KEY-----`)) func init() { var err error hostPrivateKeySigner, err = ssh.ParsePrivateKey(privKey) if err != nil { panic(err) }} func keyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { permissions := &ssh.Permissions{ CriticalOptions: map[string]string{}, Extensions: map[string]string{}, } return permissions, nil} func pwAuth(conn ssh.ConnMetadata, pw []byte) (*ssh.Permissions, error) { permissions := &ssh.Permissions{ CriticalOptions: map[string]string{}, Extensions: map[string]string{}, } return permissions, nil} func basicServerConfig() *ssh.ServerConfig { config := ssh.ServerConfig{ Config: ssh.Config{ MACs: []string{"hmac-sha1"}, }, PasswordCallback: pwAuth, PublicKeyCallback: keyAuth, } config.AddHostKey(hostPrivateKeySigner) return &config} type sshServer struct { useSubsystem bool conn net.Conn config *ssh.ServerConfig sshConn *ssh.ServerConn newChans <-chan ssh.NewChannel newReqs <-chan *ssh.Request} func sshServerFromConn(conn net.Conn, useSubsystem bool, config *ssh.ServerConfig) (*sshServer, error) { // From a standard TCP connection to an encrypted SSH connection sshConn, newChans, newReqs, err := ssh.NewServerConn(conn, config) if err != nil { return nil, err } svr := &sshServer{useSubsystem, conn, config, sshConn, newChans, newReqs} svr.listenChannels() return svr, nil} func (svr *sshServer) Wait() error { return svr.sshConn.Wait()} func (svr *sshServer) Close() error { return svr.sshConn.Close()} func (svr *sshServer) listenChannels() { go func() { for chanReq := range svr.newChans { go svr.handleChanReq(chanReq) } }() go func() { for req := range svr.newReqs { go svr.handleReq(req) } }()} func (svr *sshServer) handleReq(req *ssh.Request) { switch req.Type { default: rejectRequest(req) }} type sshChannelServer struct { svr *sshServer chanReq ssh.NewChannel ch ssh.Channel newReqs <-chan *ssh.Request} type sshSessionChannelServer struct { *sshChannelServer env []string} func (svr *sshServer) handleChanReq(chanReq ssh.NewChannel) { fmt.Fprintf(sshServerDebugStream, "channel request: %v, extra: '%v'\n", chanReq.ChannelType(), hex.EncodeToString(chanReq.ExtraData())) switch chanReq.ChannelType() { case "session": if ch, reqs, err := chanReq.Accept(); err != nil { fmt.Fprintf(sshServerDebugStream, "fail to accept channel request: %v\n", err) chanReq.Reject(ssh.ResourceShortage, "channel accept failure") } else { chsvr := &sshSessionChannelServer{ sshChannelServer: &sshChannelServer{svr, chanReq, ch, reqs}, env: append([]string{}, os.Environ()...), } chsvr.handle() } default: chanReq.Reject(ssh.UnknownChannelType, "channel type is not a session") }} func (chsvr *sshSessionChannelServer) handle() { // should maybe do something here... go chsvr.handleReqs()} func (chsvr *sshSessionChannelServer) handleReqs() { for req := range chsvr.newReqs { chsvr.handleReq(req) } fmt.Fprintf(sshServerDebugStream, "ssh server session channel complete\n")} func (chsvr *sshSessionChannelServer) handleReq(req *ssh.Request) { switch req.Type { case "env": chsvr.handleEnv(req) case "subsystem": chsvr.handleSubsystem(req) default: rejectRequest(req) }} func rejectRequest(req *ssh.Request) error { fmt.Fprintf(sshServerDebugStream, "ssh rejecting request, type: %s\n", req.Type) err := req.Reply(false, []byte{}) if err != nil { fmt.Fprintf(sshServerDebugStream, "ssh request reply had error: %v\n", err) } return err} func rejectRequestUnmarshalError(req *ssh.Request, s interface{}, err error) error { fmt.Fprintf(sshServerDebugStream, "ssh request unmarshaling error, type '%T': %v\n", s, err) rejectRequest(req) return err} // env request form:type sshEnvRequest struct { Envvar string Value string} func (chsvr *sshSessionChannelServer) handleEnv(req *ssh.Request) error { envReq := &sshEnvRequest{} if err := ssh.Unmarshal(req.Payload, envReq); err != nil { return rejectRequestUnmarshalError(req, envReq, err) } req.Reply(true, nil) found := false for i, envstr := range chsvr.env { if strings.HasPrefix(envstr, envReq.Envvar+"=") { found = true chsvr.env[i] = envReq.Envvar + "=" + envReq.Value } } if !found { chsvr.env = append(chsvr.env, envReq.Envvar+"="+envReq.Value) } return nil} // Payload: int: command size, string: commandtype sshSubsystemRequest struct { Name string} type sshSubsystemExitStatus struct { Status uint32} func (chsvr *sshSessionChannelServer) handleSubsystem(req *ssh.Request) error { defer func() { err1 := chsvr.ch.CloseWrite() err2 := chsvr.ch.Close() fmt.Fprintf(sshServerDebugStream, "ssh server subsystem request complete, err: %v %v\n", err1, err2) }() subsystemReq := &sshSubsystemRequest{} if err := ssh.Unmarshal(req.Payload, subsystemReq); err != nil { return rejectRequestUnmarshalError(req, subsystemReq, err) } // reply to the ssh client // no idea if this is actually correct spec-wise. // just enough for an sftp server to start. if subsystemReq.Name != "sftp" { return req.Reply(false, nil) } req.Reply(true, nil) if !chsvr.svr.useSubsystem { // use the openssh sftp server backend; this is to test the ssh code, not the sftp code, // or is used for comparison between our sftp subsystem and the openssh sftp subsystem cmd := exec.Command(*testSftp, "-e", "-l", "DEBUG") // log to stderr cmd.Stdin = chsvr.ch cmd.Stdout = chsvr.ch cmd.Stderr = sftpServerDebugStream if err := cmd.Start(); err != nil { return err } return cmd.Wait() } sftpServer, err := NewServer( chsvr.ch, WithDebug(sftpServerDebugStream), ) if err != nil { return err } // wait for the session to close runErr := sftpServer.Serve() exitStatus := uint32(1) if runErr == nil { exitStatus = uint32(0) } _, exitStatusErr := chsvr.ch.SendRequest("exit-status", false, ssh.Marshal(sshSubsystemExitStatus{exitStatus})) return exitStatusErr} // starts an ssh server to test. returns: host string and portfunc testServer(t *testing.T, useSubsystem bool, readonly bool) (net.Listener, string, int) { if !*testIntegration { t.Skip("skipping integration test") } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } host, portStr, err := net.SplitHostPort(listener.Addr().String()) if err != nil { t.Fatal(err) } port, err := strconv.Atoi(portStr) if err != nil { t.Fatal(err) } go func() { for { conn, err := listener.Accept() if err != nil { fmt.Fprintf(sshServerDebugStream, "ssh server socket closed: %v\n", err) break } go func() { defer conn.Close() sshSvr, err := sshServerFromConn(conn, useSubsystem, basicServerConfig()) if err != nil { t.Error(err) return } err = sshSvr.Wait() fmt.Fprintf(sshServerDebugStream, "ssh server finished, err: %v\n", err) }() } }() return listener, host, port} func makeDummyKey() (string, error) { priv, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader) if err != nil { return "", fmt.Errorf("cannot generate key: %v", err) } der, err := x509.MarshalECPrivateKey(priv) if err != nil { return "", fmt.Errorf("cannot marshal key: %v", err) } block := &pem.Block{Type: "EC PRIVATE KEY", Bytes: der} f, err := ioutil.TempFile("", "sftp-test-key-") if err != nil { return "", fmt.Errorf("cannot create temp file: %v", err) } defer func() { if f != nil { _ = f.Close() _ = os.Remove(f.Name()) } }() if err := pem.Encode(f, block); err != nil { return "", fmt.Errorf("cannot write key: %v", err) } if err := f.Close(); err != nil { return "", fmt.Errorf("error closing key file: %v", err) } path := f.Name() f = nil return path, nil} func runSftpClient(t *testing.T, script string, path string, host string, port int) (string, error) { // if sftp client binary is unavailable, skip test if _, err := os.Stat(*testSftpClientBin); err != nil { t.Skip("sftp client binary unavailable") } // make a dummy key so we don't rely on ssh-agent dummyKey, err := makeDummyKey() if err != nil { return "", err } defer os.Remove(dummyKey) args := []string{ // "-vvvv", "-b", "-", "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "UserKnownHostsFile /dev/null", // do not trigger ssh-agent prompting "-o", "IdentityFile=" + dummyKey, "-o", "IdentitiesOnly=yes", "-P", fmt.Sprintf("%d", port), fmt.Sprintf("%s:%s", host, path), } cmd := exec.Command(*testSftpClientBin, args...) var stdout bytes.Buffer cmd.Stdin = bytes.NewBufferString(script) cmd.Stdout = &stdout cmd.Stderr = sftpClientDebugStream if err := cmd.Start(); err != nil { return "", err } err = cmd.Wait() return stdout.String(), err} func TestServerCompareSubsystems(t *testing.T) { listenerGo, hostGo, portGo := testServer(t, GOLANG_SFTP, READONLY) listenerOp, hostOp, portOp := testServer(t, OPENSSH_SFTP, READONLY) defer listenerGo.Close() defer listenerOp.Close() script := `ls /ls -l /ls /dev/ls -l /dev/ls -l /etc/ls -l /bin/ls -l /usr/bin/` outputGo, err := runSftpClient(t, script, "/", hostGo, portGo) if err != nil { t.Fatal(err) } outputOp, err := runSftpClient(t, script, "/", hostOp, portOp) if err != nil { t.Fatal(err) } newlineRegex := regexp.MustCompile(`\r*\n`) spaceRegex := regexp.MustCompile(`\s+`) outputGoLines := newlineRegex.Split(outputGo, -1) outputOpLines := newlineRegex.Split(outputOp, -1) for i, goLine := range outputGoLines { if i > len(outputOpLines) { t.Fatalf("output line count differs") } opLine := outputOpLines[i] bad := false if goLine != opLine { goWords := spaceRegex.Split(goLine, -1) opWords := spaceRegex.Split(opLine, -1) // some fields are allowed to be different.. // words[2] and [3] as these are users & groups // words[1] as the link count for directories like proc is unstable // during testing as processes are created/destroyed. // words[7] as timestamp on dirs can very for things like /tmp for j, goWord := range goWords { if j > len(opWords) { bad = true } opWord := opWords[j] if goWord != opWord && j != 1 && j != 2 && j != 3 && j != 7 { bad = true } } } if bad { t.Errorf("outputs differ, go:\n%v\nopenssh:\n%v\n", goLine, opLine) } }} var rng = rand.New(rand.NewSource(time.Now().Unix())) func randData(length int) []byte { data := make([]byte, length) for i := 0; i < length; i++ { data[i] = byte(rng.Uint32()) } return dataLines 1–500 of 720. Load more
package sftp // sftp server integration tests // enable with -integration // example invokation (darwin): gofmt -w `find . -name \*.go` && (cd server_standalone/ ; go build -tags debug) && go test -tags debug github.com/pkg/sftp -integration -v -sftp /usr/libexec/sftp-server -run ServerCompareSubsystems import ( "bytes" "crypto/ecdsa" "crypto/elliptic" crand "crypto/rand" "crypto/x509" "encoding/hex" "encoding/pem" "flag" "fmt" "io/ioutil" "math/rand" "net" "os" "os/exec" "path" "path/filepath" "regexp" "strconv" "strings" "testing" "time" "github.com/kr/fs" "golang.org/x/crypto/ssh" ) var testSftpClientBin = flag.String("sftp_client", "/usr/bin/sftp", "location of the sftp client binary") var sshServerDebugStream = ioutil.Discard var sftpServerDebugStream = ioutil.Discard var sftpClientDebugStream = ioutil.Discard const ( GOLANG_SFTP = true OPENSSH_SFTP = false ) var ( hostPrivateKeySigner ssh.Signer privKey = []byte(` -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEArhp7SqFnXVZAgWREL9Ogs+miy4IU/m0vmdkoK6M97G9NX/Pj wf8I/3/ynxmcArbt8Rc4JgkjT2uxx/NqR0yN42N1PjO5Czu0dms1PSqcKIJdeUBV 7gdrKSm9Co4d2vwfQp5mg47eG4w63pz7Drk9+VIyi9YiYH4bve7WnGDswn4ycvYZ slV5kKnjlfCdPig+g5P7yQYud0cDWVwyA0+kxvL6H3Ip+Fu8rLDZn4/P1WlFAIuc PAf4uEKDGGmC2URowi5eesYR7f6GN/HnBs2776laNlAVXZUmYTUfOGagwLsEkx8x XdNqntfbs2MOOoK+myJrNtcB9pCrM0H6um19uQIDAQABAoIBABkWr9WdVKvalgkP TdQmhu3mKRNyd1wCl+1voZ5IM9Ayac/98UAvZDiNU4Uhx52MhtVLJ0gz4Oa8+i16 IkKMAZZW6ro/8dZwkBzQbieWUFJ2Fso2PyvB3etcnGU8/Yhk9IxBDzy+BbuqhYE2 1ebVQtz+v1HvVZzaD11bYYm/Xd7Y28QREVfFen30Q/v3dv7dOteDE/RgDS8Czz7w jMW32Q8JL5grz7zPkMK39BLXsTcSYcaasT2ParROhGJZDmbgd3l33zKCVc1zcj9B SA47QljGd09Tys958WWHgtj2o7bp9v1Ufs4LnyKgzrB80WX1ovaSQKvd5THTLchO kLIhUAECgYEA2doGXy9wMBmTn/hjiVvggR1aKiBwUpnB87Hn5xCMgoECVhFZlT6l WmZe7R2klbtG1aYlw+y+uzHhoVDAJW9AUSV8qoDUwbRXvBVlp+In5wIqJ+VjfivK zgIfzomL5NvDz37cvPmzqIeySTowEfbQyq7CUQSoDtE9H97E2wWZhDkCgYEAzJdJ k+NSFoTkHhfD3L0xCDHpRV3gvaOeew8524fVtVUq53X8m91ng4AX1r74dCUYwwiF gqTtSSJfx2iH1xKnNq28M9uKg7wOrCKrRqNPnYUO3LehZEC7rwUr26z4iJDHjjoB uBcS7nw0LJ+0Zeg1IF+aIdZGV3MrAKnrzWPixYECgYBsffX6ZWebrMEmQ89eUtFF u9ZxcGI/4K8ErC7vlgBD5ffB4TYZ627xzFWuBLs4jmHCeNIJ9tct5rOVYN+wRO1k /CRPzYUnSqb+1jEgILL6istvvv+DkE+ZtNkeRMXUndWwel94BWsBnUKe0UmrSJ3G sq23J3iCmJW2T3z+DpXbkQKBgQCK+LUVDNPE0i42NsRnm+fDfkvLP7Kafpr3Umdl tMY474o+QYn+wg0/aPJIf9463rwMNyyhirBX/k57IIktUdFdtfPicd2MEGETElWv nN1GzYxD50Rs2f/jKisZhEwqT9YNyV9DkgDdGGdEbJNYqbv0qpwDIg8T9foe8E1p bdErgQKBgAt290I3L316cdxIQTkJh1DlScN/unFffITwu127WMr28Jt3mq3cZpuM Aecey/eEKCj+Rlas5NDYKsB18QIuAw+qqWyq0LAKLiAvP1965Rkc4PLScl3MgJtO QYa37FK0p8NcDeUuF86zXBVutwS5nJLchHhKfd590ks57OROtm29 -----END RSA PRIVATE KEY----- `) ) func init() { var err error hostPrivateKeySigner, err = ssh.ParsePrivateKey(privKey) if err != nil { panic(err) } } func keyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { permissions := &ssh.Permissions{ CriticalOptions: map[string]string{}, Extensions: map[string]string{}, } return permissions, nil } func pwAuth(conn ssh.ConnMetadata, pw []byte) (*ssh.Permissions, error) { permissions := &ssh.Permissions{ CriticalOptions: map[string]string{}, Extensions: map[string]string{}, } return permissions, nil } func basicServerConfig() *ssh.ServerConfig { config := ssh.ServerConfig{ Config: ssh.Config{ MACs: []string{"hmac-sha1"}, }, PasswordCallback: pwAuth, PublicKeyCallback: keyAuth, } config.AddHostKey(hostPrivateKeySigner) return &config } type sshServer struct { useSubsystem bool conn net.Conn config *ssh.ServerConfig sshConn *ssh.ServerConn newChans <-chan ssh.NewChannel newReqs <-chan *ssh.Request } func sshServerFromConn(conn net.Conn, useSubsystem bool, config *ssh.ServerConfig) (*sshServer, error) { // From a standard TCP connection to an encrypted SSH connection sshConn, newChans, newReqs, err := ssh.NewServerConn(conn, config) if err != nil { return nil, err } svr := &sshServer{useSubsystem, conn, config, sshConn, newChans, newReqs} svr.listenChannels() return svr, nil } func (svr *sshServer) Wait() error { return svr.sshConn.Wait() } func (svr *sshServer) Close() error { return svr.sshConn.Close() } func (svr *sshServer) listenChannels() { go func() { for chanReq := range svr.newChans { go svr.handleChanReq(chanReq) } }() go func() { for req := range svr.newReqs { go svr.handleReq(req) } }() } func (svr *sshServer) handleReq(req *ssh.Request) { switch req.Type { default: rejectRequest(req) } } type sshChannelServer struct { svr *sshServer chanReq ssh.NewChannel ch ssh.Channel newReqs <-chan *ssh.Request } type sshSessionChannelServer struct { *sshChannelServer env []string } func (svr *sshServer) handleChanReq(chanReq ssh.NewChannel) { fmt.Fprintf(sshServerDebugStream, "channel request: %v, extra: '%v'\n", chanReq.ChannelType(), hex.EncodeToString(chanReq.ExtraData())) switch chanReq.ChannelType() { case "session": if ch, reqs, err := chanReq.Accept(); err != nil { fmt.Fprintf(sshServerDebugStream, "fail to accept channel request: %v\n", err) chanReq.Reject(ssh.ResourceShortage, "channel accept failure") } else { chsvr := &sshSessionChannelServer{ sshChannelServer: &sshChannelServer{svr, chanReq, ch, reqs}, env: append([]string{}, os.Environ()...), } chsvr.handle() } default: chanReq.Reject(ssh.UnknownChannelType, "channel type is not a session") } } func (chsvr *sshSessionChannelServer) handle() { // should maybe do something here... go chsvr.handleReqs() } func (chsvr *sshSessionChannelServer) handleReqs() { for req := range chsvr.newReqs { chsvr.handleReq(req) } fmt.Fprintf(sshServerDebugStream, "ssh server session channel complete\n") } func (chsvr *sshSessionChannelServer) handleReq(req *ssh.Request) { switch req.Type { case "env": chsvr.handleEnv(req) case "subsystem": chsvr.handleSubsystem(req) default: rejectRequest(req) } } func rejectRequest(req *ssh.Request) error { fmt.Fprintf(sshServerDebugStream, "ssh rejecting request, type: %s\n", req.Type) err := req.Reply(false, []byte{}) if err != nil { fmt.Fprintf(sshServerDebugStream, "ssh request reply had error: %v\n", err) } return err } func rejectRequestUnmarshalError(req *ssh.Request, s interface{}, err error) error { fmt.Fprintf(sshServerDebugStream, "ssh request unmarshaling error, type '%T': %v\n", s, err) rejectRequest(req) return err } // env request form: type sshEnvRequest struct { Envvar string Value string } func (chsvr *sshSessionChannelServer) handleEnv(req *ssh.Request) error { envReq := &sshEnvRequest{} if err := ssh.Unmarshal(req.Payload, envReq); err != nil { return rejectRequestUnmarshalError(req, envReq, err) } req.Reply(true, nil) found := false for i, envstr := range chsvr.env { if strings.HasPrefix(envstr, envReq.Envvar+"=") { found = true chsvr.env[i] = envReq.Envvar + "=" + envReq.Value } } if !found { chsvr.env = append(chsvr.env, envReq.Envvar+"="+envReq.Value) } return nil } // Payload: int: command size, string: command type sshSubsystemRequest struct { Name string } type sshSubsystemExitStatus struct { Status uint32 } func (chsvr *sshSessionChannelServer) handleSubsystem(req *ssh.Request) error { defer func() { err1 := chsvr.ch.CloseWrite() err2 := chsvr.ch.Close() fmt.Fprintf(sshServerDebugStream, "ssh server subsystem request complete, err: %v %v\n", err1, err2) }() subsystemReq := &sshSubsystemRequest{} if err := ssh.Unmarshal(req.Payload, subsystemReq); err != nil { return rejectRequestUnmarshalError(req, subsystemReq, err) } // reply to the ssh client // no idea if this is actually correct spec-wise. // just enough for an sftp server to start. if subsystemReq.Name != "sftp" { return req.Reply(false, nil) } req.Reply(true, nil) if !chsvr.svr.useSubsystem { // use the openssh sftp server backend; this is to test the ssh code, not the sftp code, // or is used for comparison between our sftp subsystem and the openssh sftp subsystem cmd := exec.Command(*testSftp, "-e", "-l", "DEBUG") // log to stderr cmd.Stdin = chsvr.ch cmd.Stdout = chsvr.ch cmd.Stderr = sftpServerDebugStream if err := cmd.Start(); err != nil { return err } return cmd.Wait() } sftpServer, err := NewServer( chsvr.ch, WithDebug(sftpServerDebugStream), ) if err != nil { return err } // wait for the session to close runErr := sftpServer.Serve() exitStatus := uint32(1) if runErr == nil { exitStatus = uint32(0) } _, exitStatusErr := chsvr.ch.SendRequest("exit-status", false, ssh.Marshal(sshSubsystemExitStatus{exitStatus})) return exitStatusErr } // starts an ssh server to test. returns: host string and port func testServer(t *testing.T, useSubsystem bool, readonly bool) (net.Listener, string, int) { if !*testIntegration { t.Skip("skipping integration test") } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } host, portStr, err := net.SplitHostPort(listener.Addr().String()) if err != nil { t.Fatal(err) } port, err := strconv.Atoi(portStr) if err != nil { t.Fatal(err) } go func() { for { conn, err := listener.Accept() if err != nil { fmt.Fprintf(sshServerDebugStream, "ssh server socket closed: %v\n", err) break } go func() { defer conn.Close() sshSvr, err := sshServerFromConn(conn, useSubsystem, basicServerConfig()) if err != nil { t.Error(err) return } err = sshSvr.Wait() fmt.Fprintf(sshServerDebugStream, "ssh server finished, err: %v\n", err) }() } }() return listener, host, port } func makeDummyKey() (string, error) { priv, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader) if err != nil { return "", fmt.Errorf("cannot generate key: %v", err) } der, err := x509.MarshalECPrivateKey(priv) if err != nil { return "", fmt.Errorf("cannot marshal key: %v", err) } block := &pem.Block{Type: "EC PRIVATE KEY", Bytes: der} f, err := ioutil.TempFile("", "sftp-test-key-") if err != nil { return "", fmt.Errorf("cannot create temp file: %v", err) } defer func() { if f != nil { _ = f.Close() _ = os.Remove(f.Name()) } }() if err := pem.Encode(f, block); err != nil { return "", fmt.Errorf("cannot write key: %v", err) } if err := f.Close(); err != nil { return "", fmt.Errorf("error closing key file: %v", err) } path := f.Name() f = nil return path, nil } func runSftpClient(t *testing.T, script string, path string, host string, port int) (string, error) { // if sftp client binary is unavailable, skip test if _, err := os.Stat(*testSftpClientBin); err != nil { t.Skip("sftp client binary unavailable") } // make a dummy key so we don't rely on ssh-agent dummyKey, err := makeDummyKey() if err != nil { return "", err } defer os.Remove(dummyKey) args := []string{ // "-vvvv", "-b", "-", "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "UserKnownHostsFile /dev/null", // do not trigger ssh-agent prompting "-o", "IdentityFile=" + dummyKey, "-o", "IdentitiesOnly=yes", "-P", fmt.Sprintf("%d", port), fmt.Sprintf("%s:%s", host, path), } cmd := exec.Command(*testSftpClientBin, args...) var stdout bytes.Buffer cmd.Stdin = bytes.NewBufferString(script) cmd.Stdout = &stdout cmd.Stderr = sftpClientDebugStream if err := cmd.Start(); err != nil { return "", err } err = cmd.Wait() return stdout.String(), err } func TestServerCompareSubsystems(t *testing.T) { listenerGo, hostGo, portGo := testServer(t, GOLANG_SFTP, READONLY) listenerOp, hostOp, portOp := testServer(t, OPENSSH_SFTP, READONLY) defer listenerGo.Close() defer listenerOp.Close() script := ` ls / ls -l / ls /dev/ ls -l /dev/ ls -l /etc/ ls -l /bin/ ls -l /usr/bin/ ` outputGo, err := runSftpClient(t, script, "/", hostGo, portGo) if err != nil { t.Fatal(err) } outputOp, err := runSftpClient(t, script, "/", hostOp, portOp) if err != nil { t.Fatal(err) } newlineRegex := regexp.MustCompile(`\r*\n`) spaceRegex := regexp.MustCompile(`\s+`) outputGoLines := newlineRegex.Split(outputGo, -1) outputOpLines := newlineRegex.Split(outputOp, -1) for i, goLine := range outputGoLines { if i > len(outputOpLines) { t.Fatalf("output line count differs") } opLine := outputOpLines[i] bad := false if goLine != opLine { goWords := spaceRegex.Split(goLine, -1) opWords := spaceRegex.Split(opLine, -1) // some fields are allowed to be different.. // words[2] and [3] as these are users & groups // words[1] as the link count for directories like proc is unstable // during testing as processes are created/destroyed. // words[7] as timestamp on dirs can very for things like /tmp for j, goWord := range goWords { if j > len(opWords) { bad = true } opWord := opWords[j] if goWord != opWord && j != 1 && j != 2 && j != 3 && j != 7 { bad = true } } } if bad { t.Errorf("outputs differ, go:\n%v\nopenssh:\n%v\n", goLine, opLine) } } } var rng = rand.New(rand.NewSource(time.Now().Unix())) func randData(length int) []byte { data := make([]byte, length) for i := 0; i < length; i++ { data[i] = byte(rng.Uint32()) } return data