-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsshtest_test.go
More file actions
165 lines (142 loc) · 3.53 KB
/
sshtest_test.go
File metadata and controls
165 lines (142 loc) · 3.53 KB
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
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"encoding/binary"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"golang.org/x/crypto/ssh"
)
// startTestSSHServer starts an SSH server backed by a local bare git repo.
// It handles "git-upload-pack <path>" exec requests by running git-upload-pack
// locally against repoRoot. It returns the address (host:port) of the server.
func startTestSSHServer(t *testing.T, repoRoot string) string {
t.Helper()
// Generate a host key.
hostKey, err := generateHostKey()
if err != nil {
t.Fatal(err)
}
config := &ssh.ServerConfig{
NoClientAuth: true,
}
config.AddHostKey(hostKey)
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ln.Close() })
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go handleSSHConn(t, conn, config, repoRoot)
}
}()
return ln.Addr().String()
}
func generateHostKey() (ssh.Signer, error) {
dir, err := os.MkdirTemp("", "sshtest")
if err != nil {
return nil, err
}
defer os.RemoveAll(dir)
keyFile := filepath.Join(dir, "key")
cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", keyFile, "-N", "", "-q")
if out, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("ssh-keygen: %w: %s", err, out)
}
keyPEM, err := os.ReadFile(keyFile)
if err != nil {
return nil, err
}
return ssh.ParsePrivateKey(keyPEM)
}
func handleSSHConn(t *testing.T, conn net.Conn, config *ssh.ServerConfig, repoRoot string) {
defer conn.Close()
serverConn, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
return
}
defer serverConn.Close()
go ssh.DiscardRequests(reqs)
for newChan := range chans {
if newChan.ChannelType() != "session" {
newChan.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
ch, reqs, err := newChan.Accept()
if err != nil {
return
}
go handleSSHSession(t, ch, reqs, repoRoot)
}
}
func handleSSHSession(t *testing.T, ch ssh.Channel, reqs <-chan *ssh.Request, repoRoot string) {
defer ch.Close()
for req := range reqs {
if req.Type != "exec" {
req.Reply(false, nil)
continue
}
if len(req.Payload) < 4 {
req.Reply(false, nil)
continue
}
cmdLen := binary.BigEndian.Uint32(req.Payload[:4])
if uint32(len(req.Payload)) < 4+cmdLen {
req.Reply(false, nil)
continue
}
cmdStr := string(req.Payload[4 : 4+cmdLen])
// Only allow git-upload-pack.
parts := strings.SplitN(cmdStr, " ", 2)
if len(parts) != 2 || parts[0] != "git-upload-pack" {
req.Reply(false, nil)
ch.Stderr().Write([]byte("only git-upload-pack is supported\n"))
sendExitStatus(ch, 1)
return
}
// The path may be single-quoted by ssh.
repoPath := strings.Trim(parts[1], "'")
fullPath := repoRoot + repoPath
req.Reply(true, nil)
cmd := exec.Command("git-upload-pack", fullPath)
cmd.Stdin = ch
cmd.Stdout = ch
cmd.Stderr = ch.Stderr()
var exitCode int
if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else {
exitCode = 1
}
}
sendExitStatus(ch, exitCode)
return
}
}
func sendExitStatus(ch ssh.Channel, code int) {
var buf [4]byte
binary.BigEndian.PutUint32(buf[:], uint32(code))
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// Drain any remaining input so the client doesn't block.
io.Copy(io.Discard, ch)
}()
ch.CloseWrite()
wg.Wait()
ch.SendRequest("exit-status", false, buf[:])
}