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
55package ssh
66
77import (
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.
154160func 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