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