commit adbd4da93a657bf92aa87bb3149f99aade2e8612 Author: Jeff Lindsay Date: Mon Oct 3 16:54:17 2016 -0500 initial commit diff --git a/options.go b/options.go new file mode 100644 index 0000000..bd3faea --- /dev/null +++ b/options.go @@ -0,0 +1,56 @@ +package ssh + +import "io/ioutil" + +func PasswordAuth(fn PasswordHandler) Option { + return func(srv *Server) error { + srv.PasswordHandler = fn + return nil + } +} + +func PublicKeyAuth(fn PublicKeyHandler) Option { + return func(srv *Server) error { + srv.PublicKeyHandler = fn + return nil + } +} + +func HostKeyFile(filepath string) Option { + return func(srv *Server) error { + pemBytes, err := ioutil.ReadFile(filepath) + if err != nil { + return err + } + for _, block := range decodePemBlocks(pemBytes) { + signer, err := signerFromBlock(block) + if err != nil { + return err + } + srv.HostSigners = append(srv.HostSigners, signer) + } + return nil + } +} + +func HostKeyPEM(bytes []byte) Option { + return func(srv *Server) error { + for _, block := range decodePemBlocks(bytes) { + signer, err := signerFromBlock(block) + if err != nil { + return err + } + srv.HostSigners = append(srv.HostSigners, signer) + } + return nil + } +} + +func NoPty() Option { + return func(srv *Server) error { + srv.PtyCallback = func(user string, permissions *Permissions) bool { + return false + } + return nil + } +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..d5e3f43 --- /dev/null +++ b/server.go @@ -0,0 +1,151 @@ +package ssh + +import ( + "fmt" + "net" + "time" + + gossh "golang.org/x/crypto/ssh" +) + +type Server struct { + Addr string + Handler Handler + HostSigners []Signer + PasswordHandler PasswordHandler + PublicKeyHandler PublicKeyHandler + PermissionsCallback PermissionsCallback + PtyCallback PtyCallback +} + +func (srv *Server) makeConfig() (*gossh.ServerConfig, error) { + config := &gossh.ServerConfig{} + if len(srv.HostSigners) == 0 { + signer, err := generateSigner() + if err != nil { + return nil, err + } + srv.HostSigners = append(srv.HostSigners, signer) + } + for _, signer := range srv.HostSigners { + config.AddHostKey(signer) + } + if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil { + config.NoClientAuth = true + } + if srv.PasswordHandler != nil { + config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) { + perms := &gossh.Permissions{} + if ok := srv.PasswordHandler(conn.User(), string(password)); !ok { + return perms, fmt.Errorf("permission denied") + } + if srv.PermissionsCallback != nil { + srv.PermissionsCallback(conn.User(), &Permissions{perms}) + } + return perms, nil + } + } + if srv.PublicKeyHandler != nil { + config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) { + perms := &gossh.Permissions{} + if ok := srv.PublicKeyHandler(conn.User(), key); !ok { + return perms, fmt.Errorf("permission denied") + } + perms.Extensions = map[string]string{ + "_publickey": string(key.Marshal()), + } + if srv.PermissionsCallback != nil { + srv.PermissionsCallback(conn.User(), &Permissions{perms}) + } + return perms, nil + } + } + return config, nil +} + +func (srv *Server) Handle(fn Handler) { + srv.Handler = fn +} + +func (srv *Server) Serve(l net.Listener) error { + defer l.Close() + config, err := srv.makeConfig() + if err != nil { + return err + } + if srv.Handler == nil { + srv.Handler = defaultHandler + } + var tempDelay time.Duration + for { + conn, e := l.Accept() + if e != nil { + if ne, ok := e.(net.Error); ok && ne.Temporary() { + if tempDelay == 0 { + tempDelay = 5 * time.Millisecond + } else { + tempDelay *= 2 + } + if max := 1 * time.Second; tempDelay > max { + tempDelay = max + } + //srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) + time.Sleep(tempDelay) + continue + } + return e + } + go srv.handleConn(conn, config) + } +} + +func (srv *Server) handleConn(conn net.Conn, conf *gossh.ServerConfig) { + defer conn.Close() + sshConn, chans, reqs, err := gossh.NewServerConn(conn, conf) + if err != nil { + return + } + go gossh.DiscardRequests(reqs) + for ch := range chans { + if ch.ChannelType() != "session" { + ch.Reject(gossh.UnknownChannelType, "unsupported channel type") + continue + } + go srv.handleChannel(sshConn, ch) + } +} + +func (srv *Server) handleChannel(conn *gossh.ServerConn, newChan gossh.NewChannel) { + ch, reqs, err := newChan.Accept() + if err != nil { + return + } + sess := srv.newSession(conn, ch) + sess.handleRequests(reqs) +} + +func (srv *Server) newSession(conn *gossh.ServerConn, ch gossh.Channel) *session { + sess := &session{ + Channel: ch, + conn: conn, + handler: srv.Handler, + ptyCb: srv.PtyCallback, + } + return sess +} + +func (srv *Server) ListenAndServe() error { + addr := srv.Addr + if addr == "" { + addr = ":22" + } + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + return srv.Serve(ln) +} + +func (srv *Server) SetOption(option Option) error { + return option(srv) +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..9c8fc94 --- /dev/null +++ b/session.go @@ -0,0 +1,149 @@ +package ssh + +import ( + "bytes" + "fmt" + "net" + + "github.com/anmitsu/go-shlex" + gossh "golang.org/x/crypto/ssh" +) + +type Session interface { + gossh.Channel + User() string + RemoteAddr() net.Addr + Environ() []string + Exit(code int) error + Command() []string + //Signals(c chan<- Signal) + PublicKey() PublicKey + Pty() (Pty, <-chan Window, bool) +} + +type session struct { + gossh.Channel + conn *gossh.ServerConn + handler Handler + handled bool + pty *Pty + winch chan Window + env []string + ptyCb PtyCallback + cmd []string +} + +func (sess *session) Write(p []byte) (n int, err error) { + if sess.pty != nil { + // normalize \n to \r\n when pty is accepted + p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1) + p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1) + } + return sess.Channel.Write(p) +} + +func (sess *session) PublicKey() PublicKey { + if sess.conn.Permissions == nil { + return nil + } + s, ok := sess.conn.Permissions.Extensions["_publickey"] + if !ok { + return nil + } + key, err := ParsePublicKey([]byte(s)) + if err != nil { + return nil + } + return key +} + +func (sess *session) Exit(code int) error { + 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) Environ() []string { + return append([]string(nil), sess.env...) +} + +func (sess *session) Command() []string { + return append([]string(nil), sess.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) handleRequests(reqs <-chan *gossh.Request) { + for req := range reqs { + var width, height int + var ok bool + 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.cmd, _ = shlex.Split(payload.Value, true) + go func() { + sess.handler(sess) + sess.Exit(0) + }() + sess.handled = true + req.Reply(true, nil) + 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)) + case "pty-req": + if sess.handled { + req.Reply(false, nil) + continue + } + if sess.ptyCb != nil { + ok := sess.ptyCb(sess.conn.User(), &Permissions{sess.conn.Permissions}) + if !ok { + req.Reply(false, nil) + continue + } + } + width, height, ok = parsePtyRequest(req.Payload) + if ok { + sess.pty = &Pty{Window{width, height}} + sess.winch = make(chan Window) + req.Reply(true, nil) + } + case "window-change": + if sess.pty == nil { + req.Reply(false, nil) + continue + } + width, height, ok = parseWinchRequest(req.Payload) + if ok { + sess.pty.Window = Window{width, height} + sess.winch <- sess.pty.Window + } + } + } +} diff --git a/ssh.go b/ssh.go new file mode 100644 index 0000000..5d18c64 --- /dev/null +++ b/ssh.go @@ -0,0 +1,78 @@ +package ssh + +import ( + "crypto/subtle" + "net" + + gossh "golang.org/x/crypto/ssh" +) + +type Signal string + +// POSIX signals as listed in RFC 4254 Section 6.10. +const ( + SIGABRT Signal = "ABRT" + SIGALRM Signal = "ALRM" + SIGFPE Signal = "FPE" + SIGHUP Signal = "HUP" + SIGILL Signal = "ILL" + SIGINT Signal = "INT" + SIGKILL Signal = "KILL" + SIGPIPE Signal = "PIPE" + SIGQUIT Signal = "QUIT" + SIGSEGV Signal = "SEGV" + SIGTERM Signal = "TERM" + SIGUSR1 Signal = "USR1" + SIGUSR2 Signal = "USR2" +) + +var defaultHandler Handler + +type Option func(*Server) error +type Handler func(Session) + +type PublicKeyHandler func(user string, key PublicKey) bool +type PasswordHandler func(user, password string) bool + +type PermissionsCallback func(user string, permissions *Permissions) error +type PtyCallback func(user string, permissions *Permissions) bool + +type Window struct { + Width int + Height int +} + +type Pty struct { + Window Window +} + +func Serve(l net.Listener, handler Handler, options ...Option) error { + srv := &Server{Handler: handler} + for _, option := range options { + if err := srv.SetOption(option); err != nil { + return err + } + } + return srv.Serve(l) +} + +func ListenAndServe(addr string, handler Handler, options ...Option) error { + srv := &Server{Addr: addr, Handler: handler} + for _, option := range options { + if err := srv.SetOption(option); err != nil { + return err + } + } + return srv.ListenAndServe() +} + +func Handle(handler Handler) { + defaultHandler = handler +} + +// KeysEqual is constant time compare of the keys to avoid timing attacks +func KeysEqual(ak, bk PublicKey) bool { + a := gossh.Marshal(ak) + b := gossh.Marshal(bk) + return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1) +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..b713b53 --- /dev/null +++ b/util.go @@ -0,0 +1,118 @@ +package ssh + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/binary" + "encoding/pem" + "fmt" + + "golang.org/x/crypto/ssh" +) + +func signerFromBlock(block *pem.Block) (ssh.Signer, error) { + var key interface{} + var err error + switch block.Type { + case "RSA PRIVATE KEY": + key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + case "EC PRIVATE KEY": + key, err = x509.ParseECPrivateKey(block.Bytes) + case "DSA PRIVATE KEY": + key, err = ssh.ParseDSAPrivateKey(block.Bytes) + default: + return nil, fmt.Errorf("unsupported key type %q", block.Type) + } + if err != nil { + return nil, err + } + signer, err := ssh.NewSignerFromKey(key) + if err != nil { + return nil, err + } + return signer, nil +} + +func decodePemBlocks(pemData []byte) []*pem.Block { + var blocks []*pem.Block + var block *pem.Block + for { + block, pemData = pem.Decode(pemData) + if block == nil { + return blocks + } + blocks = append(blocks, block) + } +} + +func generateSigner() (ssh.Signer, error) { + key, err := rsa.GenerateKey(rand.Reader, 768) + if err != nil { + return nil, err + } + return ssh.NewSignerFromKey(key) +} + +func parsePtyRequest(s []byte) (width, height int, ok bool) { + _, s, ok = parseString(s) + if !ok { + return + } + width32, s, ok := parseUint32(s) + if !ok { + return + } + height32, _, ok := parseUint32(s) + width = int(width32) + height = int(height32) + if width < 1 { + ok = false + } + if height < 1 { + ok = false + } + return +} + +func parseWinchRequest(s []byte) (width, height int, ok bool) { + width32, _, ok := parseUint32(s) + if !ok { + return + } + height32, _, ok := parseUint32(s) + if !ok { + return + } + + width = int(width32) + height = int(height32) + if width < 1 { + ok = false + } + if height < 1 { + ok = false + } + return +} + +func parseString(in []byte) (out string, rest []byte, ok bool) { + if len(in) < 4 { + return + } + length := binary.BigEndian.Uint32(in) + if uint32(len(in)) < 4+length { + return + } + out = string(in[4 : 4+length]) + rest = in[4+length:] + ok = true + return +} + +func parseUint32(in []byte) (uint32, []byte, bool) { + if len(in) < 4 { + return 0, nil, false + } + return binary.BigEndian.Uint32(in), in[4:], true +} diff --git a/wrap.go b/wrap.go new file mode 100644 index 0000000..b115040 --- /dev/null +++ b/wrap.go @@ -0,0 +1,23 @@ +package ssh + +import gossh "golang.org/x/crypto/ssh" + +type PublicKey interface { + gossh.PublicKey +} + +type Permissions struct { + *gossh.Permissions +} + +type Signer interface { + gossh.Signer +} + +func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) { + return gossh.ParseAuthorizedKey(in) +} + +func ParsePublicKey(in []byte) (out PublicKey, err error) { + return gossh.ParsePublicKey(in) +}