720 lines · 19.5 KB · Go
RawDownload
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 data

Lines 1–500 of 720.