aboutsummaryrefslogtreecommitdiff
path: root/rsh/sshserver_test.go
blob: f238e0b14451425d409660717582a18134d3a347 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
package main

import (
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/elliptic"
	"crypto/rand"
	"errors"
	"fmt"
	"io"
	"net"
	"os/exec"
	"testing"

	"golang.org/x/crypto/ssh"
)

// execHandler runs the remote command for one session and returns its exit
// code.
type execHandler func(cmd string, stdin io.Reader, stdout, stderr io.Writer) int

// testServer is a minimal in-process SSH server for tests: one host key, one
// accepted client public key, and a pluggable exec handler. No sshd needed.
type testServer struct {
	ln       net.Listener
	hostKey  ssh.Signer
	extraKey ssh.Signer
	authPub  ssh.PublicKey
	handle   execHandler
}

func newTestServer(t *testing.T, clientPub ssh.PublicKey, handle execHandler) *testServer {
	t.Helper()
	return newTestServerKeys(t, clientPub, handle, false)
}

// newTestServerKeys builds the server with an ed25519 host key and, when
// alsoECDSA is set, an ECDSA one too. A server offering both is the stock sshd
// case: x/crypto's default client preference would pick the ECDSA key, so this
// is what proves rsh's host-key algorithm policy is doing its job.
func newTestServerKeys(t *testing.T, clientPub ssh.PublicKey, handle execHandler, alsoECDSA bool) *testServer {
	t.Helper()
	_, hpriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	signer, err := ssh.NewSignerFromKey(hpriv)
	if err != nil {
		t.Fatal(err)
	}
	var extra ssh.Signer
	if alsoECDSA {
		epriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
		if err != nil {
			t.Fatal(err)
		}
		if extra, err = ssh.NewSignerFromKey(epriv); err != nil {
			t.Fatal(err)
		}
	}
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	s := &testServer{ln: ln, hostKey: signer, extraKey: extra, authPub: clientPub, handle: handle}
	go s.serve()
	t.Cleanup(func() { ln.Close() })
	return s
}

func (s *testServer) port() int { return s.ln.Addr().(*net.TCPAddr).Port }

func (s *testServer) serve() {
	cfg := &ssh.ServerConfig{
		PublicKeyCallback: func(_ ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
			if s.authPub != nil && string(key.Marshal()) == string(s.authPub.Marshal()) {
				return &ssh.Permissions{}, nil
			}
			return nil, fmt.Errorf("unknown public key")
		},
	}
	cfg.AddHostKey(s.hostKey)
	if s.extraKey != nil {
		cfg.AddHostKey(s.extraKey)
	}
	for {
		nConn, err := s.ln.Accept()
		if err != nil {
			return
		}
		go s.handleConn(nConn, cfg)
	}
}

func (s *testServer) handleConn(nConn net.Conn, cfg *ssh.ServerConfig) {
	conn, chans, reqs, err := ssh.NewServerConn(nConn, cfg)
	if err != nil {
		return
	}
	defer conn.Close()
	go ssh.DiscardRequests(reqs)
	for newCh := range chans {
		if newCh.ChannelType() != "session" {
			newCh.Reject(ssh.UnknownChannelType, "only session")
			continue
		}
		ch, requests, err := newCh.Accept()
		if err != nil {
			return
		}
		go s.handleSession(ch, requests)
	}
}

func (s *testServer) handleSession(ch ssh.Channel, requests <-chan *ssh.Request) {
	for req := range requests {
		if req.Type != "exec" {
			if req.WantReply {
				req.Reply(false, nil)
			}
			continue
		}
		var payload struct{ Command string }
		ssh.Unmarshal(req.Payload, &payload)
		if req.WantReply {
			req.Reply(true, nil)
		}
		code := s.handle(payload.Command, ch, ch, ch.Stderr())
		ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{uint32(code)}))
		ch.Close()
		return
	}
}

// shellExecIn runs cmd via /bin/sh -c, exactly as sshd would, wiring the
// channel to the process. Used by the real-rsync end-to-end test.
//
// stdin is fed through a detached goroutine rather than c.Stdin so that Wait
// does not block on the stdin copy: rsync's remote receiver exits while the
// client still holds the channel open, and a c.Run with a non-file Stdin would
// deadlock waiting for that copy to finish.
func shellExecIn(dir, cmd string, stdin io.Reader, stdout, stderr io.Writer) int {
	c := exec.Command("/bin/sh", "-c", cmd)
	c.Dir = dir
	c.Stdout = stdout
	c.Stderr = stderr
	inPipe, err := c.StdinPipe()
	if err != nil {
		return 1
	}
	if err := c.Start(); err != nil {
		return 1
	}
	go func() {
		io.Copy(inPipe, stdin)
		inPipe.Close()
	}()
	err = c.Wait()
	var ee *exec.ExitError
	if errors.As(err, &ee) {
		return ee.ExitCode()
	}
	if err != nil {
		return 1
	}
	return 0
}