|
|
- package ssh
-
- import (
- "bytes"
- "context"
- "errors"
- "fmt"
- "net"
- "sync"
-
- "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
-
- // LocalAddr returns the net.Addr of the server side of the connection.
- LocalAddr() 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
-
- // RawCommand returns the exact command that was provided by the user.
- RawCommand() 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.
- //
- // The context is canceled when the client's connection closes or I/O
- // operation fails.
- 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)
-
- // Signals registers a channel to receive signals sent from the client. The
- // channel must handle signal sends or it will block the SSH request loop.
- // Registering nil will unregister the channel from signal sends. During the
- // time no channel is registered signals are buffered up to a reasonable amount.
- // If there are buffered signals when a channel is registered, they will be
- // sent in order on the channel immediately after registering.
- Signals(c chan<- Signal)
- }
-
- // maxSigBufSize is how many signals will be buffered
- // when there is no signal channel specified
- const maxSigBufSize = 128
-
- func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
- 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,
- sessReqCb: srv.SessionRequestCallback,
- ctx: ctx,
- }
- sess.handleRequests(reqs)
- }
-
- type session struct {
- sync.Mutex
- gossh.Channel
- conn *gossh.ServerConn
- handler Handler
- handled bool
- exited bool
- pty *Pty
- winch chan Window
- env []string
- ptyCb PtyCallback
- sessReqCb SessionRequestCallback
- rawCmd string
- ctx Context
- sigCh chan<- Signal
- sigBuf []Signal
- }
-
- 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
- }
-
- func (sess *session) Exit(code int) error {
- sess.Lock()
- defer sess.Unlock()
- 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) LocalAddr() net.Addr {
- return sess.conn.LocalAddr()
- }
-
- func (sess *session) Environ() []string {
- return append([]string(nil), sess.env...)
- }
-
- func (sess *session) RawCommand() string {
- return sess.rawCmd
- }
-
- func (sess *session) Command() []string {
- cmd, _ := shlex.Split(sess.rawCmd, true)
- return append([]string(nil), 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) Signals(c chan<- Signal) {
- sess.Lock()
- defer sess.Unlock()
- sess.sigCh = c
- if len(sess.sigBuf) > 0 {
- go func() {
- for _, sig := range sess.sigBuf {
- sess.sigCh <- sig
- }
- }()
- }
- }
-
- 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
- }
-
- var payload = struct{ Value string }{}
- gossh.Unmarshal(req.Payload, &payload)
- sess.rawCmd = payload.Value
-
- // If there's a session policy callback, we need to confirm before
- // accepting the session.
- if sess.sessReqCb != nil && !sess.sessReqCb(sess, req.Type) {
- sess.rawCmd = ""
- req.Reply(false, nil)
- continue
- }
-
- sess.handled = true
- req.Reply(true, nil)
-
- 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 "signal":
- var payload struct{ Signal string }
- gossh.Unmarshal(req.Payload, &payload)
- sess.Lock()
- if sess.sigCh != nil {
- sess.sigCh <- Signal(payload.Signal)
- } else {
- if len(sess.sigBuf) < maxSigBufSize {
- sess.sigBuf = append(sess.sigBuf, Signal(payload.Signal))
- }
- }
- sess.Unlock()
- 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.ctx)
- req.Reply(true, nil)
- default:
- // TODO: debug log
- req.Reply(false, nil)
- }
- }
- }
|