github.com/jiasir/deis@v1.12.2/builder/sshd/server.go (about) 1 /*Package sshd implements an SSH server. 2 3 See https://tools.ietf.org/html/rfc4254 4 5 This was copied over (and effectively forked from) cookoo-ssh. Mainly this 6 differs from the cookoo-ssh version in that this does not act like a 7 stand-alone SSH server. 8 */ 9 package sshd 10 11 import ( 12 "encoding/binary" 13 "fmt" 14 "net" 15 "strings" 16 "sync" 17 "text/template" 18 19 "github.com/Masterminds/cookoo" 20 "github.com/Masterminds/cookoo/log" 21 "github.com/Masterminds/cookoo/safely" 22 "golang.org/x/crypto/ssh" 23 ) 24 25 const ( 26 // HostKeys is the context key for Host Keys list. 27 HostKeys string = "ssh.HostKeys" 28 // Address is the context key for SSH address. 29 Address string = "ssh.Address" 30 // ServerConfig is the context key for ServerConfig object. 31 ServerConfig string = "ssh.ServerConfig" 32 ) 33 34 // PrereceiveHookTmpl is a pre-receive hook. 35 const PrereceiveHookTpl = `#!/bin/bash 36 strip_remote_prefix() { 37 stdbuf -i0 -o0 -e0 sed "s/^/"$'\e[1G'"/" 38 } 39 40 echo "pre-receive hook START" 41 set -eo pipefail; while read oldrev newrev refname; do 42 [[ $refname = "refs/heads/master" ]] && git archive $newrev | {{.Receiver}} "$RECEIVE_REPO" "$newrev" | strip_remote_prefix 43 done 44 echo "pre-receive hook END" 45 ` 46 47 // Serve starts a native SSH server. 48 // 49 // The general design of the server is that it acts as a main server for 50 // a Cookoo app. It assumes that certain things have been configured for it, 51 // like an ssh.ServerConfig. Once it runs, it will block until the main 52 // process terminates. If you want to stop it prior to that, you can grab 53 // the closer ("sshd.Closer") out of the context and send it a signal. 54 // 55 // Currently, the service is not generic. It only runs git hooks. 56 // 57 // This expects the following Context variables. 58 // - ssh.Hostkeys ([]ssh.Signer): Host key, as an unparsed byte slice. 59 // - ssh.Address (string): Address/port 60 // - ssh.ServerConfig (*ssh.ServerConfig): The server config to use. 61 // 62 // This puts the following variables into the context: 63 // - ssh.Closer (chan interface{}): Send a message to this to shutdown the server. 64 func Serve(reg *cookoo.Registry, router *cookoo.Router, c cookoo.Context) cookoo.Interrupt { 65 hostkeys := c.Get(HostKeys, []ssh.Signer{}).([]ssh.Signer) 66 addr := c.Get(Address, "0.0.0.0:2223").(string) 67 cfg := c.Get(ServerConfig, &ssh.ServerConfig{}).(*ssh.ServerConfig) 68 69 for _, hk := range hostkeys { 70 cfg.AddHostKey(hk) 71 log.Infof(c, "Added hostkey.") 72 } 73 74 listener, err := net.Listen("tcp", addr) 75 if err != nil { 76 return err 77 } 78 79 srv := &server{ 80 c: c, 81 gitHome: "/home/git", 82 } 83 84 closer := make(chan interface{}, 1) 85 c.Put("sshd.Closer", closer) 86 87 log.Infof(c, "Listening on %s", addr) 88 srv.listen(listener, cfg, closer) 89 90 return nil 91 } 92 93 // server is the struct that encapsulates the SSH server. 94 type server struct { 95 c cookoo.Context 96 gitHome string 97 hookTpl *template.Template 98 createLock sync.Mutex 99 } 100 101 // listen handles accepting and managing connections. However, since closer 102 // is len(1), it will not block the sender. 103 func (s *server) listen(l net.Listener, conf *ssh.ServerConfig, closer chan interface{}) error { 104 cxt := s.c 105 log.Info(cxt, "Accepting new connections.") 106 defer l.Close() 107 108 // FIXME: Since Accept blocks, closer may not be checked often enough. 109 for { 110 log.Info(cxt, "Checking closer.") 111 if len(closer) > 0 { 112 <-closer 113 log.Info(cxt, "Shutting down SSHD listener.") 114 return nil 115 } 116 conn, err := l.Accept() 117 if err != nil { 118 log.Warnf(cxt, "Error during Accept: %s", err) 119 // We shouldn't kill the listener because of an error. 120 return err 121 } 122 safely.GoDo(cxt, func() { 123 s.handleConn(conn, conf) 124 }) 125 } 126 } 127 128 // handleConn handles an individual client connection. 129 // 130 // It manages the connection, but passes channels on to `answer()`. 131 func (s *server) handleConn(conn net.Conn, conf *ssh.ServerConfig) { 132 defer conn.Close() 133 log.Info(s.c, "Accepted connection.") 134 _, chans, reqs, err := ssh.NewServerConn(conn, conf) 135 if err != nil { 136 // Handshake failure. 137 log.Errf(s.c, "Failed handshake: %s (%v)", err, conn) 138 return 139 } 140 141 // Discard global requests. We're only concerned with channels. 142 safely.GoDo(s.c, func() { ssh.DiscardRequests(reqs) }) 143 144 condata := sshConnection(conn) 145 146 // Now we handle the channels. 147 for incoming := range chans { 148 log.Infof(s.c, "Channel type: %s\n", incoming.ChannelType()) 149 if incoming.ChannelType() != "session" { 150 incoming.Reject(ssh.UnknownChannelType, "Unknown channel type") 151 } 152 153 channel, req, err := incoming.Accept() 154 if err != nil { 155 // Should close request and move on. 156 panic(err) 157 } 158 safely.GoDo(s.c, func() { s.answer(channel, req, condata) }) 159 } 160 conn.Close() 161 } 162 163 // sshConnection generates the SSH_CONNECTION environment variable. 164 // 165 // This is untested on UNIX sockets. 166 func sshConnection(conn net.Conn) string { 167 remote := conn.RemoteAddr().String() 168 local := conn.LocalAddr().String() 169 rhost, rport, _ := net.SplitHostPort(remote) 170 lhost, lport, _ := net.SplitHostPort(local) 171 172 return fmt.Sprintf("%s %d %s %d", rhost, rport, lhost, lport) 173 } 174 175 func sendExitStatus(status uint32, channel ssh.Channel) error { 176 exit := struct{ Status uint32 }{uint32(0)} 177 _, err := channel.SendRequest("exit-status", false, ssh.Marshal(exit)) 178 return err 179 } 180 181 // answer handles answering requests and channel requests 182 // 183 // Currently, an exec must be either "ping", "git-receive-pack" or 184 // "git-upload-pack". Anything else will result in a failure response. Right 185 // now, we leave the channel open on failure because it is unclear what the 186 // correct behavior for a failed exec is. 187 // 188 // Support for setting environment variables via `env` has been disabled. 189 func (s *server) answer(channel ssh.Channel, requests <-chan *ssh.Request, sshConn string) error { 190 defer channel.Close() 191 192 // Answer all the requests on this connection. 193 for req := range requests { 194 ok := false 195 196 // I think that ideally what we want to do here is pass this on to 197 // the Cookoo router and let it handle each Type on its own. 198 switch req.Type { 199 case "env": 200 o := &EnvVar{} 201 ssh.Unmarshal(req.Payload, o) 202 fmt.Printf("Key='%s', Value='%s'\n", o.Name, o.Value) 203 req.Reply(true, nil) 204 case "exec": 205 clean := cleanExec(req.Payload) 206 parts := strings.SplitN(clean, " ", 2) 207 208 router := s.c.Get("cookoo.Router", nil).(*cookoo.Router) 209 210 // TODO: Should we unset the context value 'cookoo.Router'? 211 // We need a shallow copy of the context to avoid race conditions. 212 cxt := s.c.Copy() 213 cxt.Put("SSH_CONNECTION", sshConn) 214 215 // Only allow commands that we know about. 216 switch parts[0] { 217 case "ping": 218 cxt.Put("channel", channel) 219 cxt.Put("request", req) 220 sshPing := cxt.Get("route.sshd.sshPing", "sshPing").(string) 221 err := router.HandleRequest(sshPing, cxt, true) 222 if err != nil { 223 log.Warnf(s.c, "Error pinging: %s", err) 224 } 225 return err 226 case "git-receive-pack", "git-upload-pack": 227 if len(parts) < 2 { 228 log.Warn(s.c, "Expected two-part command.\n") 229 req.Reply(ok, nil) 230 break 231 } 232 req.Reply(true, nil) // We processed. Yay. 233 234 cxt.Put("channel", channel) 235 cxt.Put("request", req) 236 cxt.Put("operation", parts[0]) 237 cxt.Put("repository", parts[1]) 238 sshGitReceive := cxt.Get("route.sshd.sshGitReceive", "sshGitReceive").(string) 239 err := router.HandleRequest(sshGitReceive, cxt, true) 240 var xs uint32 241 if err != nil { 242 log.Errf(s.c, "Failed git receive: %v", err) 243 xs = 1 244 } 245 sendExitStatus(xs, channel) 246 return nil 247 default: 248 log.Warnf(s.c, "Illegal command is '%s'\n", clean) 249 req.Reply(false, nil) 250 return nil 251 } 252 253 if err := sendExitStatus(0, channel); err != nil { 254 log.Errf(s.c, "Failed to write exit status: %s", err) 255 } 256 return nil 257 default: 258 // We simply ignore all of the other cases and leave the 259 // channel open to take additional requests. 260 log.Infof(s.c, "Received request of type %s\n", req.Type) 261 req.Reply(false, nil) 262 } 263 } 264 265 return nil 266 } 267 268 // ExecCmd is an SSH exec request 269 type ExecCmd struct { 270 Value string 271 } 272 273 // EnvVar is an SSH env request 274 type EnvVar struct { 275 Name string 276 Value string 277 } 278 279 // GenericMessage describes a simple string message, which is common in SSH. 280 type GenericMessage struct { 281 Value string 282 } 283 284 // cleanExec cleans the exec string. 285 func cleanExec(pay []byte) string { 286 e := &ExecCmd{} 287 ssh.Unmarshal(pay, e) 288 // TODO: Minimal escaping of values in command. There is probably a better 289 // way of doing this. 290 r := strings.NewReplacer("$", "", "`", "'") 291 return r.Replace(e.Value) 292 } 293 294 // parseString parses an encoded string according to the indicated length. 295 // From ssh.Unmarshal. 296 func parseString(in []byte) (out, rest []byte, ok bool) { 297 if len(in) < 4 { 298 return 299 } 300 length := binary.BigEndian.Uint32(in) 301 if uint32(len(in)) < 4+length { 302 return 303 } 304 out = in[4 : 4+length] 305 rest = in[4+length:] 306 ok = true 307 return 308 } 309 310 // parseEnv parses the key/value pairs in env requests. 311 func parseEnv(pay []byte) ([]byte, []byte) { 312 313 l := pay[3] 314 315 key := pay[4 : 4+l] 316 317 offset := l + 8 318 l = pay[7+l] // 4 for the offset, l for the key, 3 for the next three bytes. 319 val := pay[offset : l+offset] 320 321 return key, val 322 323 } 324 325 // Ping handles a simple test SSH exec. 326 // 327 // Returns the string PONG and exit status 0. 328 // 329 // Params: 330 // - channel (ssh.Channel): The channel to respond on. 331 // - request (*ssh.Request): The request. 332 // 333 func Ping(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 334 channel := p.Get("channel", nil).(ssh.Channel) 335 req := p.Get("request", nil).(*ssh.Request) 336 log.Info(c, "PING\n") 337 if _, err := channel.Write([]byte("pong")); err != nil { 338 log.Errf(c, "Failed to write to channel: %s", err) 339 } 340 sendExitStatus(0, channel) 341 req.Reply(true, nil) 342 return nil, nil 343 }