glider-ssh/session.go

375 lines
9.3 KiB
Go
Raw Permalink Normal View History

2021-11-19 01:07:30 +00:00
package glider_ssh
2016-10-03 21:54:17 +00:00
import (
"bytes"
"context"
"errors"
2016-10-03 21:54:17 +00:00
"fmt"
"net"
"sync"
2016-10-03 21:54:17 +00:00
"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
2020-02-13 10:51:32 +00:00
// crypto/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
2016-10-03 21:54:17 +00:00
type Session interface {
gossh.Channel
// User returns the username used when establishing the SSH connection.
2016-10-03 21:54:17 +00:00
User() string
// RemoteAddr returns the net.Addr of the client side of the connection.
2016-10-03 21:54:17 +00:00
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".
2016-10-03 21:54:17 +00:00
Environ() []string
// Exit sends an exit status and then closes the session.
2016-10-03 21:54:17 +00:00
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.
2016-10-03 21:54:17 +00:00
Command() []string
2019-06-19 07:20:00 +00:00
// RawCommand returns the exact command that was provided by the user.
RawCommand() string
2019-06-19 08:19:34 +00:00
// Subsystem returns the subsystem requested by the user.
Subsystem() string
// PublicKey returns the PublicKey used to authenticate. If a public key was not
// used it will return nil.
2016-10-03 21:54:17 +00:00
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.
2016-10-03 21:54:17 +00:00
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)
// Break regisers a channel to receive notifications of break requests sent
// from the client. The channel must handle break requests, or it will block
// the request handling loop. Registering nil will unregister the channel.
// During the time that no channel is registered, breaks are ignored.
Break(c chan<- bool)
2016-10-03 21:54:17 +00:00
}
// maxSigBufSize is how many signals will be buffered
// when there is no signal channel specified
const maxSigBufSize = 128
func DefaultSessionHandler(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{
2019-06-19 08:19:34 +00:00
Channel: ch,
conn: conn,
handler: srv.Handler,
ptyCb: srv.PtyCallback,
sessReqCb: srv.SessionRequestCallback,
subsystemHandlers: srv.SubsystemHandlers,
ctx: ctx,
}
sess.handleRequests(reqs)
}
2016-10-03 21:54:17 +00:00
type session struct {
sync.Mutex
2016-10-03 21:54:17 +00:00
gossh.Channel
2019-06-19 08:19:34 +00:00
conn *gossh.ServerConn
handler Handler
subsystemHandlers map[string]SubsystemHandler
handled bool
exited bool
pty *Pty
winch chan Window
env []string
ptyCb PtyCallback
sessReqCb SessionRequestCallback
rawCmd string
subsystem string
ctx Context
sigCh chan<- Signal
sigBuf []Signal
breakCh chan<- bool
2016-10-03 21:54:17 +00:00
}
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.
2016-10-03 21:54:17 +00:00
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
2016-10-03 21:54:17 +00:00
}
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
2016-10-03 21:54:17 +00:00
}
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
2016-10-03 21:54:17 +00:00
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()
}
2016-10-03 21:54:17 +00:00
func (sess *session) Environ() []string {
return append([]string(nil), sess.env...)
}
2019-06-19 07:20:00 +00:00
func (sess *session) RawCommand() string {
return sess.rawCmd
}
2016-10-03 21:54:17 +00:00
func (sess *session) Command() []string {
2019-06-19 07:20:00 +00:00
cmd, _ := shlex.Split(sess.rawCmd, true)
return append([]string(nil), cmd...)
2016-10-03 21:54:17 +00:00
}
2019-06-19 08:19:34 +00:00
func (sess *session) Subsystem() string {
return sess.subsystem
}
2016-10-03 21:54:17 +00:00
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) Break(c chan<- bool) {
sess.Lock()
defer sess.Unlock()
sess.breakCh = c
}
2016-10-03 21:54:17 +00:00
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
}
2016-10-03 21:54:17 +00:00
var payload = struct{ Value string }{}
gossh.Unmarshal(req.Payload, &payload)
2019-06-19 07:20:00 +00:00
sess.rawCmd = payload.Value
// If there's a session policy callback, we need to confirm before
// accepting the session.
if sess.sessReqCb != nil && !sess.sessReqCb(sess, req.Type) {
2019-06-19 07:20:00 +00:00
sess.rawCmd = ""
req.Reply(false, nil)
continue
}
sess.handled = true
req.Reply(true, nil)
2016-10-03 21:54:17 +00:00
go func() {
sess.handler(sess)
sess.Exit(0)
}()
2019-06-19 08:19:34 +00:00
case "subsystem":
if sess.handled {
req.Reply(false, nil)
continue
}
var payload = struct{ Value string }{}
gossh.Unmarshal(req.Payload, &payload)
sess.subsystem = payload.Value
// If there's a session policy callback, we need to confirm before
// accepting the session.
if sess.sessReqCb != nil && !sess.sessReqCb(sess, req.Type) {
sess.rawCmd = ""
req.Reply(false, nil)
continue
}
handler := sess.subsystemHandlers[payload.Value]
if handler == nil {
handler = sess.subsystemHandlers["default"]
}
if handler == nil {
req.Reply(false, nil)
continue
}
sess.handled = true
req.Reply(true, nil)
go func() {
handler(sess)
sess.Exit(0)
}()
2016-10-03 21:54:17 +00:00
case "env":
if sess.handled {
req.Reply(false, nil)
continue
}
var kv struct{ Key, Value string }
2016-10-03 21:54:17 +00:00
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()
2016-10-03 21:54:17 +00:00
case "pty-req":
if sess.handled || sess.pty != nil {
2016-10-03 21:54:17 +00:00
req.Reply(false, nil)
continue
}
ptyReq, ok := parsePtyRequest(req.Payload)
if !ok {
req.Reply(false, nil)
continue
}
2016-10-03 21:54:17 +00:00
if sess.ptyCb != nil {
ok := sess.ptyCb(sess.ctx, ptyReq)
2016-10-03 21:54:17 +00:00
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)
2016-10-03 21:54:17 +00:00
case "window-change":
if sess.pty == nil {
req.Reply(false, nil)
continue
}
win, ok := parseWinchRequest(req.Payload)
2016-10-03 21:54:17 +00:00
if ok {
sess.pty.Window = win
sess.winch <- win
2016-10-03 21:54:17 +00:00
}
req.Reply(ok, nil)
case agentRequestType:
// TODO: option/callback to allow agent forwarding
SetAgentRequested(sess.ctx)
req.Reply(true, nil)
case "break":
ok := false
sess.Lock()
if sess.breakCh != nil {
sess.breakCh <- true
ok = true
}
req.Reply(ok, nil)
sess.Unlock()
default:
// TODO: debug log
Remote forwarding (#88) * context: fixed documentation to be more specific about ContextKeyConn being the key for a gossh.ServerConn Signed-off-by: Jeff Lindsay <progrium@gmail.com> * server: fixes handler setup, changed to interface based handlers, added global request handler map * tcpip: working remote forwarding Signed-off-by: Jeff Lindsay <progrium@gmail.com> * context: docs typo Signed-off-by: Jeff Lindsay <progrium@gmail.com> * session: always reply to unblock clients trying something Signed-off-by: Jeff Lindsay <progrium@gmail.com> * tcpip: stop listening when ssh clients disconnect Signed-off-by: Jeff Lindsay <progrium@gmail.com> * Remote forwarding (#87) * Update generateSigner key size to 2048 (#62) Fixes #58 * Add syntax highlighting to readme (#67) * small api updates (#69) These updates make it easier to implement and pass custom Session and Context implementations No compatibilty breaking, all tests pass * Move channelHandlers to avoid data race (#59) * Update tests to work with go 1.10+ (#73) Fixes #72 * Update shutdown to use a WaitGroup rather than sleeping (#74) * Fix race condition in TestServerClose (#75) In test server close, 3 things need to happen in order: - Client session start - Server.Close - Client session exit (With io.EOF) This fix ensures the client won't do anything until after the call to close which ensure's we'll get io.EOF rather than a different error. * Update circleci config to test multiple go versions * Update CircleCI config to test 1.9 and the latest The x/crypto/ssh library dropped support go < 1.9 as that's the first version to have the math/bits library. https://github.com/golang/crypto/commit/83c378c48d6ee2ca9c20551b599aa74cb7765785 * Wait for connections to finish when shutting down PR #74 introduced a WaitGroup for listeners, but it doesn't wait for open connections before closing the server. This patch waits until all conns are closed before returning from Shutdown. * Support port forwarding of literal IPv6 addresses (#85) * Support port forwarding of literal IPv6 addresses To disambiguate between colons as host:port separators and as IPv6 address separators, literal IPv6 addresses use square brackets around the address (https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers). So host ::1, port 22 is written as [::1]:22, and therefore a simple concatenation of host, colon, and port doesn't work. Fortunately net.JoinHostPort already implements this functionality, so with a bit of type gymnastics we can generate dest in an IPv6-safe way. * Support port forwarding of literal IPv6 addresses To disambiguate between colons as host:port separators and as IPv6 address separators, literal IPv6 addresses use square brackets around the address (https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers). So host ::1, port 22 is written as [::1]:22, and therefore a simple concatenation of host, colon, and port doesn't work. Fortunately net.JoinHostPort already implements this functionality, so with a bit of type gymnastics we can generate dest in an IPv6-safe way. * Reverse port forwarding callback added * garbage removed
2018-11-13 16:04:02 +00:00
req.Reply(false, nil)
2016-10-03 21:54:17 +00:00
}
}
}