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