package ssh import ( "bytes" "context" "errors" "fmt" "net" "github.com/anmitsu/go-shlex" gossh "golang.org/x/crypto/ssh" ) // Session provides access to information about an SSH session and methods // to read and write to the SSH channel with an embedded Channel interface from // cypto/ssh. // // When Command() returns an empty slice, the user requested a shell. Otherwise // the user is performing an exec with those command arguments. // // TODO: Signals type Session interface { gossh.Channel // User returns the username used when establishing the SSH connection. User() string // RemoteAddr returns the net.Addr of the client side of the connection. RemoteAddr() net.Addr // Environ returns a copy of strings representing the environment set by the // user for this session, in the form "key=value". Environ() []string // Exit sends an exit status and then closes the session. Exit(code int) error // Command returns a shell parsed slice of arguments that were provided by the // user. Shell parsing splits the command string according to POSIX shell rules, // which considers quoting not just whitespace. Command() []string // PublicKey returns the PublicKey used to authenticate. If a public key was not // used it will return nil. PublicKey() PublicKey // Context returns the connection's context. The returned context is always // non-nil and holds the same data as the Context passed into auth // handlers and callbacks. Context() context.Context // Permissions returns a copy of the Permissions object that was available for // setup in the auth handlers via the Context. Permissions() Permissions // Pty returns PTY information, a channel of window size changes, and a boolean // of whether or not a PTY was accepted for this session. Pty() (Pty, <-chan Window, bool) // TODO: Signals(c chan<- Signal) } func sessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx *sshContext) { ch, reqs, err := newChan.Accept() if err != nil { // TODO: trigger event callback return } sess := &session{ Channel: ch, conn: conn, handler: srv.Handler, ptyCb: srv.PtyCallback, ctx: ctx, } sess.handleRequests(reqs) } type session struct { gossh.Channel conn *gossh.ServerConn handler Handler handled bool exited bool pty *Pty winch chan Window env []string ptyCb PtyCallback cmd []string ctx *sshContext } func (sess *session) Write(p []byte) (n int, err error) { if sess.pty != nil { m := len(p) // normalize \n to \r\n when pty is accepted. // this is a hardcoded shortcut since we don't support terminal modes. p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1) p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1) n, err = sess.Channel.Write(p) if n > m { n = m } return } return sess.Channel.Write(p) } func (sess *session) PublicKey() PublicKey { sessionkey := sess.ctx.Value(ContextKeyPublicKey) if sessionkey == nil { return nil } return sessionkey.(PublicKey) } func (sess *session) Permissions() Permissions { // use context permissions because its properly // wrapped and easier to dereference perms := sess.ctx.Value(ContextKeyPermissions).(*Permissions) return *perms } func (sess *session) Context() context.Context { return sess.ctx.Context } func (sess *session) Exit(code int) error { if sess.exited { return errors.New("Session.Exit called multiple times") } sess.exited = true status := struct{ Status uint32 }{uint32(code)} _, err := sess.SendRequest("exit-status", false, gossh.Marshal(&status)) if err != nil { return err } return sess.Close() } func (sess *session) User() string { return sess.conn.User() } func (sess *session) RemoteAddr() net.Addr { return sess.conn.RemoteAddr() } func (sess *session) Environ() []string { return append([]string(nil), sess.env...) } func (sess *session) Command() []string { return append([]string(nil), sess.cmd...) } func (sess *session) Pty() (Pty, <-chan Window, bool) { if sess.pty != nil { return *sess.pty, sess.winch, true } return Pty{}, sess.winch, false } func (sess *session) handleRequests(reqs <-chan *gossh.Request) { for req := range reqs { switch req.Type { case "shell", "exec": if sess.handled { req.Reply(false, nil) continue } sess.handled = true req.Reply(true, nil) var payload = struct{ Value string }{} gossh.Unmarshal(req.Payload, &payload) sess.cmd, _ = shlex.Split(payload.Value, true) go func() { sess.handler(sess) sess.Exit(0) }() case "env": if sess.handled { req.Reply(false, nil) continue } var kv = struct{ Key, Value string }{} gossh.Unmarshal(req.Payload, &kv) sess.env = append(sess.env, fmt.Sprintf("%s=%s", kv.Key, kv.Value)) req.Reply(true, nil) case "pty-req": if sess.handled || sess.pty != nil { req.Reply(false, nil) continue } ptyReq, ok := parsePtyRequest(req.Payload) if !ok { req.Reply(false, nil) continue } if sess.ptyCb != nil { ok := sess.ptyCb(sess.ctx, ptyReq) if !ok { req.Reply(false, nil) continue } } sess.pty = &ptyReq sess.winch = make(chan Window, 1) sess.winch <- ptyReq.Window defer func() { // when reqs is closed close(sess.winch) }() req.Reply(ok, nil) case "window-change": if sess.pty == nil { req.Reply(false, nil) continue } win, ok := parseWinchRequest(req.Payload) if ok { sess.pty.Window = win sess.winch <- win } req.Reply(ok, nil) case agentRequestType: // TODO: option/callback to allow agent forwarding setAgentRequested(sess) req.Reply(true, nil) default: // TODO: debug log } } }