diff options
Diffstat (limited to 'teleirc/matterbridge/vendor/github.com/shazow/ssh-chat/sshd/terminal.go')
| -rw-r--r-- | teleirc/matterbridge/vendor/github.com/shazow/ssh-chat/sshd/terminal.go | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/teleirc/matterbridge/vendor/github.com/shazow/ssh-chat/sshd/terminal.go b/teleirc/matterbridge/vendor/github.com/shazow/ssh-chat/sshd/terminal.go new file mode 100644 index 0000000..e8d9901 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/shazow/ssh-chat/sshd/terminal.go @@ -0,0 +1,246 @@ +package sshd + +import ( + "errors" + "fmt" + "net" + "sync" + "time" + + "github.com/shazow/ssh-chat/sshd/terminal" + "golang.org/x/crypto/ssh" +) + +var keepaliveInterval = time.Second * 30 +var keepaliveRequest = "keepalive@ssh-chat" + +// ErrNoSessionChannel is returned when there is no session channel. +var ErrNoSessionChannel = errors.New("no session channel") + +// ErrNotSessionChannel is returned when a channel is not a session channel. +var ErrNotSessionChannel = errors.New("terminal requires session channel") + +// Connection is an interface with fields necessary to operate an sshd host. +type Connection interface { + PublicKey() ssh.PublicKey + RemoteAddr() net.Addr + Name() string + ClientVersion() []byte + Close() error +} + +type sshConn struct { + *ssh.ServerConn +} + +func (c sshConn) PublicKey() ssh.PublicKey { + if c.Permissions == nil { + return nil + } + + s, ok := c.Permissions.Extensions["pubkey"] + if !ok { + return nil + } + + key, err := ssh.ParsePublicKey([]byte(s)) + if err != nil { + return nil + } + + return key +} + +func (c sshConn) Name() string { + return c.User() +} + +// EnvVar is an environment variable key-value pair +type EnvVar struct { + Key string + Value string +} + +func (v EnvVar) String() string { + return v.Key + "=" + v.Value +} + +// Env is a wrapper type around []EnvVar with some helper methods +type Env []EnvVar + +// Get returns the latest value for a given key, or empty string if not found +func (e Env) Get(key string) string { + for i := len(e) - 1; i >= 0; i-- { + if e[i].Key == key { + return e[i].Value + } + } + return "" +} + +// Terminal extends ssh/terminal to include a close method +type Terminal struct { + terminal.Terminal + Conn Connection + Channel ssh.Channel + + done chan struct{} + closeOnce sync.Once + + mu sync.Mutex + env []EnvVar + term string +} + +// Make new terminal from a session channel +// TODO: For v2, make a separate `Serve(ctx context.Context) error` method to activate the Terminal +func NewTerminal(conn *ssh.ServerConn, ch ssh.NewChannel) (*Terminal, error) { + if ch.ChannelType() != "session" { + return nil, ErrNotSessionChannel + } + channel, requests, err := ch.Accept() + if err != nil { + return nil, err + } + term := Terminal{ + Terminal: *terminal.NewTerminal(channel, ""), + Conn: sshConn{conn}, + Channel: channel, + + done: make(chan struct{}), + } + + ready := make(chan struct{}) + go term.listen(requests, ready) + + go func() { + // Keep-Alive Ticker + ticker := time.Tick(keepaliveInterval) + for { + select { + case <-ticker: + _, err := channel.SendRequest(keepaliveRequest, true, nil) + if err != nil { + // Connection is gone + logger.Printf("[%s] Keepalive failed, closing terminal: %s", term.Conn.RemoteAddr(), err) + term.Close() + return + } + case <-term.done: + return + } + } + }() + + // We need to wait for term.ready to acquire a shell before we return, this + // gives the SSH session a chance to populate the env vars and other state. + // TODO: Make the timeout configurable + // TODO: Use context.Context for abort/timeout in the future, will need to change the API. + select { + case <-ready: // shell acquired + return &term, nil + case <-term.done: + return nil, errors.New("terminal aborted") + case <-time.NewTimer(time.Minute).C: + return nil, errors.New("timed out starting terminal") + } +} + +// NewSession Finds a session channel and make a Terminal from it +func NewSession(conn *ssh.ServerConn, channels <-chan ssh.NewChannel) (*Terminal, error) { + // Make a terminal from the first session found + for ch := range channels { + if t := ch.ChannelType(); t != "session" { + logger.Printf("[%s] Ignored channel type: %s", conn.RemoteAddr(), t) + ch.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) + continue + } + + return NewTerminal(conn, ch) + } + + return nil, ErrNoSessionChannel +} + +// Close terminal and ssh connection +func (t *Terminal) Close() error { + var err error + t.closeOnce.Do(func() { + close(t.done) + t.Channel.Close() + err = t.Conn.Close() + }) + return err +} + +// listen negotiates the terminal type and state +// ready is closed when the terminal is ready. +func (t *Terminal) listen(requests <-chan *ssh.Request, ready chan<- struct{}) { + hasShell := false + + for req := range requests { + var width, height int + var ok bool + + switch req.Type { + case "shell": + if !hasShell { + ok = true + hasShell = true + close(ready) + } + case "pty-req": + var term string + term, width, height, ok = parsePtyRequest(req.Payload) + if ok { + // TODO: Hardcode width to 100000? + err := t.SetSize(width, height) + ok = err == nil + // Save the term: + t.mu.Lock() + t.term = term + t.mu.Unlock() + } + case "window-change": + width, height, ok = parseWinchRequest(req.Payload) + if ok { + // TODO: Hardcode width to 100000? + err := t.SetSize(width, height) + ok = err == nil + } + case "env": + var v EnvVar + if err := ssh.Unmarshal(req.Payload, &v); err == nil { + t.mu.Lock() + t.env = append(t.env, v) + t.mu.Unlock() + ok = true + } + } + + if req.WantReply { + req.Reply(ok, nil) + } + } +} + +// Env returns a list of environment key-values that have been set. They are +// returned in the order that they have been set, there is no deduplication or +// other pre-processing applied. +func (t *Terminal) Env() Env { + t.mu.Lock() + defer t.mu.Unlock() + return Env(t.env) +} + +// Term returns the terminal string value as set by the pty. +// If there was no pty request, it falls back to the TERM value passed in as an +// Env variable. +func (t *Terminal) Term() string { + t.mu.Lock() + defer t.mu.Unlock() + if t.term != "" { + return t.term + } + return Env(t.env).Get("TERM") +} |
