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  }