Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
308 lines
7.6 KiB
308 lines
7.6 KiB
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 |
|
// cypto/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 |
|
|
|
// 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) |
|
} |
|
|
|
// 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, |
|
ctx: ctx, |
|
} |
|
sess.handleRequests(reqs) |
|
} |
|
|
|
type session struct { |
|
sync.Mutex |
|
gossh.Channel |
|
conn *gossh.ServerConn |
|
handler Handler |
|
handled bool |
|
exited bool |
|
pty *Pty |
|
winch chan Window |
|
env []string |
|
ptyCb PtyCallback |
|
sessReqCb SessionRequestCallback |
|
rawCmd string |
|
ctx Context |
|
sigCh chan<- Signal |
|
sigBuf []Signal |
|
} |
|
|
|
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) 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) 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 "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) |
|
default: |
|
// TODO: debug log |
|
req.Reply(false, nil) |
|
} |
|
} |
|
}
|
|
|