Skip to content

Commit 296e803

Browse files
committed
Switch built-in SSH server to use github.com/gliderlabs/ssh
Signed-off-by: Kaleb Elwert <[email protected]>
1 parent 0e46499 commit 296e803

File tree

6 files changed

+147
-148
lines changed

6 files changed

+147
-148
lines changed

cmd/serv.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func parseCmd(cmd string) (string, string) {
7070
if len(ss) != 2 {
7171
return "", ""
7272
}
73-
return ss[0], strings.Replace(ss[1], "'/", "'", 1)
73+
return ss[0], ss[1]
7474
}
7575

7676
var (
@@ -138,7 +138,7 @@ func runServ(c *cli.Context) error {
138138
}
139139
}
140140

141-
repoPath := strings.ToLower(strings.Trim(args, "'"))
141+
repoPath := strings.ToLower(strings.TrimLeft(strings.Trim(args, "'"), "/"))
142142
rr := strings.SplitN(repoPath, "/", 2)
143143
if len(rr) != 2 {
144144
fail("Invalid repository path", "Invalid repository path: %v", args)

integrations/mysql.ini.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ LFS_CONTENT_PATH = data/lfs-mysql
3434
OFFLINE_MODE = false
3535
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
3636
APP_DATA_PATH = integrations/gitea-integration-mysql/data
37+
BUILTIN_SSH_SERVER_USER = git
3738

3839
[mailer]
3940
ENABLED = false

integrations/pgsql.ini.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ LFS_CONTENT_PATH = data/lfs-pgsql
3434
OFFLINE_MODE = false
3535
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
3636
APP_DATA_PATH = integrations/gitea-integration-pgsql/data
37+
BUILTIN_SSH_SERVER_USER = git
3738

3839
[mailer]
3940
ENABLED = false

integrations/repo_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) {
7373
assert.Equal(t, setting.AppURL+"user2/repo1.git", link)
7474
link, exists = htmlDoc.doc.Find("#repo-clone-ssh").Attr("data-link")
7575
assert.True(t, exists, "The template has changed")
76-
sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.RunUser, setting.SSH.Domain, setting.SSH.Port)
76+
sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.BuiltinServerUser, setting.SSH.Domain, setting.SSH.Port)
7777
assert.Equal(t, sshURL, link)
7878
}
7979

integrations/sqlite.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ LFS_CONTENT_PATH = data/lfs-sqlite
3030
OFFLINE_MODE = false
3131
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
3232
APP_DATA_PATH = integrations/gitea-integration-sqlite/data
33+
BUILTIN_SSH_SERVER_USER = git
3334

3435
[mailer]
3536
ENABLED = false

modules/ssh/ssh.go

Lines changed: 141 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,170 +1,174 @@
1-
// Copyright 2014 The Gogs Authors. All rights reserved.
1+
// Copyright 2018 The Gitea Authors. All rights reserved.
22
// Use of this source code is governed by a MIT-style
33
// license that can be found in the LICENSE file.
44

55
package ssh
66

77
import (
88
"io"
9-
"io/ioutil"
10-
"net"
119
"os"
1210
"os/exec"
1311
"path/filepath"
1412
"strings"
15-
16-
"github.com/Unknwon/com"
17-
"golang.org/x/crypto/ssh"
13+
"sync"
14+
"syscall"
15+
"unicode"
1816

1917
"code.gitea.io/gitea/models"
2018
"code.gitea.io/gitea/modules/log"
2119
"code.gitea.io/gitea/modules/setting"
20+
21+
"github.com/Unknwon/com"
22+
"github.com/gliderlabs/ssh"
23+
gossh "golang.org/x/crypto/ssh"
2224
)
2325

24-
func cleanCommand(cmd string) string {
25-
i := strings.Index(cmd, "git")
26-
if i == -1 {
27-
return cmd
26+
type contextKey string
27+
28+
const giteaKeyID = contextKey("gitea-key-id")
29+
30+
func getExitStatusFromError(err error) int {
31+
if err == nil {
32+
return 0
2833
}
29-
return cmd[i:]
30-
}
3134

32-
func handleServerConn(keyID string, chans <-chan ssh.NewChannel) {
33-
for newChan := range chans {
34-
if newChan.ChannelType() != "session" {
35-
newChan.Reject(ssh.UnknownChannelType, "unknown channel type")
36-
continue
37-
}
35+
exitErr, ok := err.(*exec.ExitError)
36+
if !ok {
37+
return 1
38+
}
3839

39-
ch, reqs, err := newChan.Accept()
40-
if err != nil {
41-
log.Error(3, "Error accepting channel: %v", err)
42-
continue
40+
waitStatus, ok := exitErr.Sys().(syscall.WaitStatus)
41+
if !ok {
42+
// This is a fallback and should at least let us return something useful
43+
// when running on Windows, even if it isn't completely accurate.
44+
if exitErr.Success() {
45+
return 0
4346
}
4447

45-
go func(in <-chan *ssh.Request) {
46-
defer ch.Close()
47-
for req := range in {
48-
payload := cleanCommand(string(req.Payload))
49-
switch req.Type {
50-
case "env":
51-
args := strings.Split(strings.Replace(payload, "\x00", "", -1), "\v")
52-
if len(args) != 2 {
53-
log.Warn("SSH: Invalid env arguments: '%#v'", args)
54-
continue
55-
}
56-
args[0] = strings.TrimLeft(args[0], "\x04")
57-
_, _, err := com.ExecCmdBytes("env", args[0]+"="+args[1])
58-
if err != nil {
59-
log.Error(3, "env: %v", err)
60-
return
61-
}
62-
case "exec":
63-
cmdName := strings.TrimLeft(payload, "'()")
64-
log.Trace("SSH: Payload: %v", cmdName)
65-
66-
args := []string{"serv", "key-" + keyID, "--config=" + setting.CustomConf}
67-
log.Trace("SSH: Arguments: %v", args)
68-
cmd := exec.Command(setting.AppPath, args...)
69-
cmd.Env = append(
70-
os.Environ(),
71-
"SSH_ORIGINAL_COMMAND="+cmdName,
72-
"SKIP_MINWINSVC=1",
73-
)
74-
75-
stdout, err := cmd.StdoutPipe()
76-
if err != nil {
77-
log.Error(3, "SSH: StdoutPipe: %v", err)
78-
return
79-
}
80-
stderr, err := cmd.StderrPipe()
81-
if err != nil {
82-
log.Error(3, "SSH: StderrPipe: %v", err)
83-
return
84-
}
85-
input, err := cmd.StdinPipe()
86-
if err != nil {
87-
log.Error(3, "SSH: StdinPipe: %v", err)
88-
return
89-
}
90-
91-
// FIXME: check timeout
92-
if err = cmd.Start(); err != nil {
93-
log.Error(3, "SSH: Start: %v", err)
94-
return
95-
}
96-
97-
req.Reply(true, nil)
98-
go io.Copy(input, ch)
99-
io.Copy(ch, stdout)
100-
io.Copy(ch.Stderr(), stderr)
101-
102-
if err = cmd.Wait(); err != nil {
103-
log.Error(3, "SSH: Wait: %v", err)
104-
return
105-
}
106-
107-
ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0})
108-
return
109-
default:
110-
}
111-
}
112-
}(reqs)
48+
return 1
11349
}
50+
51+
return waitStatus.ExitStatus()
11452
}
11553

116-
func listen(config *ssh.ServerConfig, host string, port int) {
117-
listener, err := net.Listen("tcp", host+":"+com.ToStr(port))
54+
func shellEscape(args []string) string {
55+
var data = make([]string, len(args))
56+
for k, v := range args {
57+
data[k] = v
58+
59+
// This is a borderline dirty hack. It's designed to only escape
60+
// strings which *might* need to be escaped. It uses a very
61+
// limited character set so everything that needs to be quoted
62+
// will be but we can ignore some simple cases to make it easier
63+
// to parse in the ssh hook. We allow alpha-numeric characters
64+
// along with ., _, and -
65+
idx := strings.IndexFunc(v, func(r rune) bool {
66+
return !unicode.IsLetter(r) && !unicode.IsDigit(r) && !strings.ContainsRune(".-_", r)
67+
})
68+
if idx > -1 || v == "" {
69+
data[k] = "'" + strings.Replace(data[k], "'", "'\"'\"'", -1) + "'"
70+
}
71+
}
72+
return strings.Join(data, " ")
73+
}
74+
75+
func sessionHandler(session ssh.Session) {
76+
keyID := session.Context().Value(giteaKeyID).(int64)
77+
78+
command := shellEscape(session.Command())
79+
80+
log.Trace("SSH: Payload: %v", command)
81+
82+
args := []string{"serv", "key-" + com.ToStr(keyID), "--config=" + setting.CustomConf}
83+
log.Trace("SSH: Arguments: %v", args)
84+
cmd := exec.Command(setting.AppPath, args...)
85+
cmd.Env = append(
86+
os.Environ(),
87+
"SSH_ORIGINAL_COMMAND="+command,
88+
"SKIP_MINWINSVC=1",
89+
)
90+
91+
stdout, err := cmd.StdoutPipe()
11892
if err != nil {
119-
log.Fatal(4, "Failed to start SSH server: %v", err)
93+
log.Error(3, "SSH: StdoutPipe: %v", err)
94+
return
95+
}
96+
stderr, err := cmd.StderrPipe()
97+
if err != nil {
98+
log.Error(3, "SSH: StderrPipe: %v", err)
99+
return
100+
}
101+
stdin, err := cmd.StdinPipe()
102+
if err != nil {
103+
log.Error(3, "SSH: StdinPipe: %v", err)
104+
return
120105
}
121-
for {
122-
// Once a ServerConfig has been configured, connections can be accepted.
123-
conn, err := listener.Accept()
124-
if err != nil {
125-
log.Error(3, "SSH: Error accepting incoming connection: %v", err)
126-
continue
127-
}
128106

129-
// Before use, a handshake must be performed on the incoming net.Conn.
130-
// It must be handled in a separate goroutine,
131-
// otherwise one user could easily block entire loop.
132-
// For example, user could be asked to trust server key fingerprint and hangs.
133-
go func() {
134-
log.Trace("SSH: Handshaking for %s", conn.RemoteAddr())
135-
sConn, chans, reqs, err := ssh.NewServerConn(conn, config)
136-
if err != nil {
137-
if err == io.EOF {
138-
log.Warn("SSH: Handshaking was terminated: %v", err)
139-
} else {
140-
log.Error(3, "SSH: Error on handshaking: %v", err)
141-
}
142-
return
143-
}
144-
145-
log.Trace("SSH: Connection from %s (%s)", sConn.RemoteAddr(), sConn.ClientVersion())
146-
// The incoming Request channel must be serviced.
147-
go ssh.DiscardRequests(reqs)
148-
go handleServerConn(sConn.Permissions.Extensions["key-id"], chans)
149-
}()
107+
wg := &sync.WaitGroup{}
108+
wg.Add(2)
109+
110+
if err = cmd.Start(); err != nil {
111+
log.Error(3, "SSH: Start: %v", err)
112+
return
113+
}
114+
115+
go func() {
116+
defer stdin.Close()
117+
io.Copy(stdin, session)
118+
}()
119+
120+
go func() {
121+
defer wg.Done()
122+
io.Copy(session, stdout)
123+
}()
124+
125+
go func() {
126+
defer wg.Done()
127+
io.Copy(session.Stderr(), stderr)
128+
}()
129+
130+
// Ensure all the output has been written before we wait on the command
131+
// to exit.
132+
wg.Wait()
133+
134+
// Wait for the command to exit and log any errors we get
135+
err = cmd.Wait()
136+
if err != nil {
137+
log.Error(3, "SSH: Wait: %v", err)
138+
}
139+
140+
session.Exit(getExitStatusFromError(err))
141+
}
142+
143+
func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
144+
if ctx.User() != setting.SSH.BuiltinServerUser {
145+
return false
150146
}
147+
148+
pkey, err := models.SearchPublicKeyByContent(strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))))
149+
if err != nil {
150+
log.Error(3, "SearchPublicKeyByContent: %v", err)
151+
return false
152+
}
153+
154+
ctx.SetValue(giteaKeyID, pkey.ID)
155+
156+
return true
151157
}
152158

153159
// Listen starts a SSH server listens on given port.
154160
func Listen(host string, port int, ciphers []string, keyExchanges []string, macs []string) {
155-
config := &ssh.ServerConfig{
156-
Config: ssh.Config{
157-
Ciphers: ciphers,
158-
KeyExchanges: keyExchanges,
159-
MACs: macs,
160-
},
161-
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
162-
pkey, err := models.SearchPublicKeyByContent(strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key))))
163-
if err != nil {
164-
log.Error(3, "SearchPublicKeyByContent: %v", err)
165-
return nil, err
166-
}
167-
return &ssh.Permissions{Extensions: map[string]string{"key-id": com.ToStr(pkey.ID)}}, nil
161+
// TODO: Handle ciphers, keyExchanges, and macs
162+
163+
srv := ssh.Server{
164+
Addr: host + ":" + com.ToStr(port),
165+
PublicKeyHandler: publicKeyHandler,
166+
Handler: sessionHandler,
167+
168+
// We need to explicitly disable the PtyCallback so text displays
169+
// properly.
170+
PtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool {
171+
return false
168172
},
169173
}
170174

@@ -180,18 +184,10 @@ func Listen(host string, port int, ciphers []string, keyExchanges []string, macs
180184
if err != nil {
181185
log.Fatal(4, "Failed to generate private key: %v - %s", err, stderr)
182186
}
183-
log.Trace("SSH: New private key is generateed: %s", keyPath)
187+
log.Trace("SSH: New private key is generated: %s", keyPath)
184188
}
185189

186-
privateBytes, err := ioutil.ReadFile(keyPath)
187-
if err != nil {
188-
log.Fatal(4, "SSH: Failed to load private key")
189-
}
190-
private, err := ssh.ParsePrivateKey(privateBytes)
191-
if err != nil {
192-
log.Fatal(4, "SSH: Failed to parse private key")
193-
}
194-
config.AddHostKey(private)
190+
srv.SetOption(ssh.HostKeyFile(keyPath))
195191

196-
go listen(config, host, port)
192+
go srv.ListenAndServe()
197193
}

0 commit comments

Comments
 (0)