summaryrefslogtreecommitdiff
path: root/teleirc/matterbridge/vendor/github.com/shazow/ssh-chat/sshd/terminal.go
diff options
context:
space:
mode:
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.go246
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")
+}