matterbridge/vendor/github.com/shazow/ssh-chat/sshd/terminal.go

247 lines
5.5 KiB
Go
Raw Normal View History

2017-12-03 00:24:05 +00:00
package sshd
import (
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/shazow/ssh-chat/sshd/terminal"
2017-12-03 00:24:05 +00:00
"golang.org/x/crypto/ssh"
)
var keepaliveInterval = time.Second * 30
var keepaliveRequest = "keepalive@ssh-chat"
// ErrNoSessionChannel is returned when there is no session channel.
2017-12-03 00:24:05 +00:00
var ErrNoSessionChannel = errors.New("no session channel")
// ErrNotSessionChannel is returned when a channel is not a session channel.
2017-12-03 00:24:05 +00:00
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()
}
2020-11-22 14:55:57 +00:00
// 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
2017-12-03 00:24:05 +00:00
type Terminal struct {
terminal.Terminal
Conn Connection
Channel ssh.Channel
done chan struct{}
closeOnce sync.Once
2020-11-22 14:55:57 +00:00
mu sync.Mutex
env []EnvVar
term string
2017-12-03 00:24:05 +00:00
}
// Make new terminal from a session channel
2020-11-22 14:55:57 +00:00
// TODO: For v2, make a separate `Serve(ctx context.Context) error` method to activate the Terminal
2017-12-03 00:24:05 +00:00
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{
2020-11-22 14:55:57 +00:00
Terminal: *terminal.NewTerminal(channel, ""),
2017-12-03 00:24:05 +00:00
Conn: sshConn{conn},
Channel: channel,
done: make(chan struct{}),
}
2020-11-22 14:55:57 +00:00
ready := make(chan struct{})
go term.listen(requests, ready)
2017-12-03 00:24:05 +00:00
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
}
}
}()
2020-11-22 14:55:57 +00:00
// 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")
}
2017-12-03 00:24:05 +00:00
}
// NewSession Finds a session channel and make a Terminal from it
2017-12-03 00:24:05 +00:00
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
}
2020-11-22 14:55:57 +00:00
// 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{}) {
2017-12-03 00:24:05 +00:00
hasShell := false
for req := range requests {
var width, height int
var ok bool
switch req.Type {
case "shell":
if !hasShell {
ok = true
hasShell = true
2020-11-22 14:55:57 +00:00
close(ready)
2017-12-03 00:24:05 +00:00
}
case "pty-req":
2020-11-22 14:55:57 +00:00
var term string
term, width, height, ok = parsePtyRequest(req.Payload)
2017-12-03 00:24:05 +00:00
if ok {
// TODO: Hardcode width to 100000?
err := t.SetSize(width, height)
ok = err == nil
2020-11-22 14:55:57 +00:00
// Save the term:
t.mu.Lock()
t.term = term
t.mu.Unlock()
2017-12-03 00:24:05 +00:00
}
case "window-change":
width, height, ok = parseWinchRequest(req.Payload)
if ok {
// TODO: Hardcode width to 100000?
err := t.SetSize(width, height)
ok = err == nil
}
2020-11-22 14:55:57 +00:00
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
}
2017-12-03 00:24:05 +00:00
}
if req.WantReply {
req.Reply(ok, nil)
}
}
}
2020-11-22 14:55:57 +00:00
// 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")
}