forked from Shiloh/githaven
f825f20d49
* Upgrade Gliderlabs SSH to 0.3.3 and add FailedConnectionCallback Following the merging of https://github.com/gliderlabs/ssh/pull/143 we can now report connections to the ssh server that have failed before public key exchange has completed using the standard fail2ban message. This PR updates Gliderlabs SSH and adds a callback that will provide this logging. Signed-off-by: Andrew Thornton <art27@cantab.net> * move the callback to its own function to make the logging appear little nicer Signed-off-by: Andrew Thornton <art27@cantab.net>
375 lines
9.3 KiB
Go
Vendored
375 lines
9.3 KiB
Go
Vendored
package ssh
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
|
|
"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
|
|
// 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
|
|
type Session interface {
|
|
gossh.Channel
|
|
|
|
// User returns the username used when establishing the SSH connection.
|
|
User() string
|
|
|
|
// RemoteAddr returns the net.Addr of the client side of the connection.
|
|
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".
|
|
Environ() []string
|
|
|
|
// Exit sends an exit status and then closes the session.
|
|
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.
|
|
Command() []string
|
|
|
|
// RawCommand returns the exact command that was provided by the user.
|
|
RawCommand() string
|
|
|
|
// 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.
|
|
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.
|
|
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)
|
|
}
|
|
|
|
// 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{
|
|
Channel: ch,
|
|
conn: conn,
|
|
handler: srv.Handler,
|
|
ptyCb: srv.PtyCallback,
|
|
sessReqCb: srv.SessionRequestCallback,
|
|
subsystemHandlers: srv.SubsystemHandlers,
|
|
ctx: ctx,
|
|
}
|
|
sess.handleRequests(reqs)
|
|
}
|
|
|
|
type session struct {
|
|
sync.Mutex
|
|
gossh.Channel
|
|
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
|
|
}
|
|
|
|
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.
|
|
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
|
|
}
|
|
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
|
|
}
|
|
|
|
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
|
|
|
|
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()
|
|
}
|
|
|
|
func (sess *session) Environ() []string {
|
|
return append([]string(nil), sess.env...)
|
|
}
|
|
|
|
func (sess *session) RawCommand() string {
|
|
return sess.rawCmd
|
|
}
|
|
|
|
func (sess *session) Command() []string {
|
|
cmd, _ := shlex.Split(sess.rawCmd, true)
|
|
return append([]string(nil), cmd...)
|
|
}
|
|
|
|
func (sess *session) Subsystem() string {
|
|
return sess.subsystem
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var payload = struct{ Value string }{}
|
|
gossh.Unmarshal(req.Payload, &payload)
|
|
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) {
|
|
sess.rawCmd = ""
|
|
req.Reply(false, nil)
|
|
continue
|
|
}
|
|
|
|
sess.handled = true
|
|
req.Reply(true, nil)
|
|
|
|
go func() {
|
|
sess.handler(sess)
|
|
sess.Exit(0)
|
|
}()
|
|
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)
|
|
}()
|
|
case "env":
|
|
if sess.handled {
|
|
req.Reply(false, nil)
|
|
continue
|
|
}
|
|
var kv struct{ Key, Value string }
|
|
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()
|
|
case "pty-req":
|
|
if sess.handled || sess.pty != nil {
|
|
req.Reply(false, nil)
|
|
continue
|
|
}
|
|
ptyReq, ok := parsePtyRequest(req.Payload)
|
|
if !ok {
|
|
req.Reply(false, nil)
|
|
continue
|
|
}
|
|
if sess.ptyCb != nil {
|
|
ok := sess.ptyCb(sess.ctx, ptyReq)
|
|
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)
|
|
case "window-change":
|
|
if sess.pty == nil {
|
|
req.Reply(false, nil)
|
|
continue
|
|
}
|
|
win, ok := parseWinchRequest(req.Payload)
|
|
if ok {
|
|
sess.pty.Window = win
|
|
sess.winch <- win
|
|
}
|
|
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
|
|
req.Reply(false, nil)
|
|
}
|
|
}
|
|
}
|