47df570d18
These updates make it easier to implement and pass custom Session and Context implementations No compatibilty breaking, all tests pass
288 lines
7.1 KiB
Go
288 lines
7.1 KiB
Go
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
|
|
|
|
// 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 sessionHandler(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,
|
|
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
|
|
cmd []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) 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) 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
|
|
}
|
|
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 "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
|
|
}
|
|
}
|
|
}
|