--- a/go.mod Tue Sep 17 22:50:27 2019 -0500
+++ b/go.mod Wed Sep 18 09:12:26 2019 -0500
@@ -2,7 +2,10 @@
github.com/alecthomas/kong v0.1.16
+ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect github.com/casbin/casbin/v2 v2.0.2
+ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/gliderlabs/ssh v0.2.2 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/sirupsen/logrus v1.4.1
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
--- a/go.sum Tue Sep 17 22:50:27 2019 -0500
+++ b/go.sum Wed Sep 18 09:12:26 2019 -0500
@@ -2,12 +2,18 @@
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/alecthomas/kong v0.1.16 h1:BheBKuvr6FE1unlZVdqkdZo/D/eDu90rrVIlpPbOdgw=
github.com/alecthomas/kong v0.1.16/go.mod h1:0m2VYms8rH0qbCqVB2gvGHk74bqLIq0HXjCs5bNbNQU=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/casbin/casbin/v2 v2.0.2 h1:TDRkBDCnsh3yWrdVHnIh8TL0I50u5kW3eRHBrw2ctME=
github.com/casbin/casbin/v2 v2.0.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
--- a/ssh/commands/commands.go Tue Sep 17 22:50:27 2019 -0500
+++ b/ssh/commands/commands.go Wed Sep 18 09:12:26 2019 -0500
@@ -4,8 +4,8 @@
"github.com/alecthomas/kong"
+ "github.com/gliderlabs/ssh" "github.com/kballard/go-shellquote"
- "golang.org/x/crypto/ssh"
@@ -21,8 +21,7 @@
- Run(conn ssh.Channel, serverConn *ssh.ServerConn, username string, req *ssh.Request) error
+ Run(session ssh.Session, username string) error func parse(cmd string) (cli, string, error) {
--- a/ssh/commands/init.go Tue Sep 17 22:50:27 2019 -0500
+++ b/ssh/commands/init.go Wed Sep 18 09:12:26 2019 -0500
@@ -4,7 +4,7 @@
- "golang.org/x/crypto/ssh"
+ "github.com/gliderlabs/ssh" "bitbucket.org/rw_grim/hgkeeper/access"
"bitbucket.org/rw_grim/hgkeeper/hg"
@@ -22,14 +22,10 @@
-func (i *Init) Run(conn ssh.Channel, serverConn *ssh.ServerConn, username string, req *ssh.Request) error {
+func (i *Init) Run(session ssh.Session, username string) error { if !access.CanInit(username, "/"+i.repoName) {
return fmt.Errorf("access denied")
- return run(hg.Init(i.repoPath), conn, serverConn, req)
+ return run(hg.Init(i.repoPath), session)
-func (i *Init) String() string {
--- a/ssh/commands/run.go Tue Sep 17 22:50:27 2019 -0500
+++ b/ssh/commands/run.go Wed Sep 18 09:12:26 2019 -0500
@@ -2,89 +2,67 @@
+ "github.com/gliderlabs/ssh" log "github.com/sirupsen/logrus"
- "golang.org/x/crypto/ssh"
"bitbucket.org/rw_grim/hgkeeper/hg"
-func run(hgCmd *hg.Command, conn ssh.Channel, serverConn *ssh.ServerConn, req *ssh.Request) error {
+func run(hgCmd *hg.Command, session ssh.Session) error { if err := hgCmd.Setup(); err != nil {
- if err := cmd.Wait(); err != nil {
- "%s command %q failed: %v",
- serverConn.RemoteAddr(),
- strings.Join(cmd.Args, " "),
- "%s command %s finished",
- serverConn.RemoteAddr(),
- strings.Join(cmd.Args, " "),
+ stdout, err := cmd.StdoutPipe() - stdinWriter, err := cmd.StdinPipe()
+ stderr, err := cmd.StderrPipe() + stdin, err := cmd.StdinPipe() - stdoutReader, err := cmd.StdoutPipe()
+ wg := &sync.WaitGroup{} + // we wait for stderr and stdout to finish - stderrReader, err := cmd.StderrPipe()
+ if err := cmd.Start(); err != nil {
- // now wire up stdin/stdout/stderr
- io.Copy(stdinWriter, conn)
+ if _, err := io.Copy(stdin, session); err != nil { + log.Errorf("Failed to read stdin from session: %v", err) - io.Copy(conn, stdoutReader)
+ if _, err := io.Copy(session, stdout); err != nil { + log.Errorf("Failed to write stdout to session: %v", err) - io.Copy(conn.Stderr(), stderrReader)
+ if _, err := io.Copy(session.Stderr(), stderr); err != nil { + log.Errorf("Failed to write stderr to session: %v", err) - if err := cmd.Start(); err != nil {
+ // wait until all output is processed
--- a/ssh/commands/serve.go Tue Sep 17 22:50:27 2019 -0500
+++ b/ssh/commands/serve.go Wed Sep 18 09:12:26 2019 -0500
@@ -4,8 +4,8 @@
+ "github.com/gliderlabs/ssh" log "github.com/sirupsen/logrus"
- "golang.org/x/crypto/ssh"
"bitbucket.org/rw_grim/hgkeeper/access"
"bitbucket.org/rw_grim/hgkeeper/hg"
@@ -23,14 +23,14 @@
-func (s *Serve) Run(conn ssh.Channel, serverConn *ssh.ServerConn, username string, req *ssh.Request) error {
+func (s *Serve) Run(session ssh.Session, username string) error { if !access.CanRead(username, "/"+s.repoName) {
return fmt.Errorf("repository %q not found", s.repoName)
writeable := access.CanWrite(username, "/"+s.repoName)
- if err := run(hg.Serve(s.repoPath, writeable), conn, serverConn, req); err != nil {
+ if err := run(hg.Serve(s.repoPath, writeable), session); err != nil { @@ -42,7 +42,3 @@
-func (s *Serve) String() string {
--- a/ssh/keys.go Tue Sep 17 22:50:27 2019 -0500
+++ b/ssh/keys.go Wed Sep 18 09:12:26 2019 -0500
@@ -33,7 +33,7 @@
+ s.server.AddHostKey(key) log.Infof("added host key from %s", path)
--- a/ssh/server.go Tue Sep 17 22:50:27 2019 -0500
+++ b/ssh/server.go Wed Sep 18 09:12:26 2019 -0500
@@ -1,11 +1,9 @@
+ "github.com/gliderlabs/ssh" log "github.com/sirupsen/logrus"
- "golang.org/x/crypto/ssh"
+ gossh "golang.org/x/crypto/ssh" "bitbucket.org/rw_grim/hgkeeper/access"
"bitbucket.org/rw_grim/hgkeeper/ssh/commands"
@@ -13,8 +11,7 @@
func NewServer(hostKeysPath, reposPath, adminRepo string) (*Server, error) {
@@ -22,164 +19,100 @@
- s.cfg = &ssh.ServerConfig{
- PublicKeyCallback: s.publicKeyCallback,
+ s.server = &ssh.Server{ + PublicKeyHandler: s.publicKeyHandler, + Handler: s.sessionHandler, + PtyCallback: s.ptyCallback,
- if err = s.setHostKeysPath(hostKeysPath); err != nil {
+ if err := s.setHostKeysPath(hostKeysPath); err != nil { -func (s *Server) Listen(addr string) error {
- listener, err := net.Listen("tcp", addr)
+// ptyCallback returns false because we don't support pty's. +func (s *Server) ptyCallback(ctx ssh.Context, pty ssh.Pty) bool { +func (s *Server) publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { + username, err := access.UsernameFromPubkey(key)
+ log.Warnf("authentication failure, unknown key %s", gossh.FingerprintSHA256(key))
- log.Infof("listening for ssh connections on %s", addr)
- tcpConn, err := s.listener.Accept()
- "failed to accept ssh connection for %s: %v",
- sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, s.cfg)
- "ssh handshake failed for %q : %v",
- "new ssh connection for %q from %s(%s)",
- sshConn.Permissions.Extensions["username"],
- sshConn.ClientVersion(),
- go ssh.DiscardRequests(reqs)
- go s.processSSHChannels(chans, sshConn)
-func (s *Server) publicKeyCallback(meta ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
- username, err := access.UsernameFromPubkey(key)
+ ctx.SetValue("username", username) "%q authenticated with %s",
- ssh.FingerprintSHA256(key),
+ gossh.FingerprintSHA256(key), - return &ssh.Permissions{
- Extensions: map[string]string{"username": username},
-func (s *Server) processSSHChannels(chans <-chan ssh.NewChannel, serverConn *ssh.ServerConn) {
- for ch := range chans {
- // we process each channel in a go routine as there's network traffic involved
- go s.processSSHChannel(ch, serverConn)
-func (s *Server) processSSHChannel(ch ssh.NewChannel, serverConn *ssh.ServerConn) {
- if t := ch.ChannelType(); t != "session" {
- ch.Reject(ssh.UnknownChannelType, fmt.Sprintf("unsupported channel type %s", t))
+func (s *Server) sessionHandler(session ssh.Session) { + // per the docs, session.Command is empty if the user is requesting a shell. + // we only support execs, which means command should be non-empty. + if len(session.Command()) == 0 { - conn, requests, err := ch.Accept()
+ username := session.Context().Value("username").(string) + "%s@%s requested command %q", + cmd, err := commands.Find(session.RawCommand(), s.reposPath) - log.Warnf("failed to accept connection: %v", err)
+ log.Warnf("failed to find command for %q, %v", session.RawCommand(), err) - // now run through all of the requests but only handle shell requests
- for req := range requests {
- username := serverConn.Permissions.Extensions["username"]
- // this is garbage, but payload is a pascal string where the
- // first 4 bytes are the length of the string followed by the
- // data. the string() method converts it, but includes the
- // length, so we just strip it off ahead of time and everything
- rawCmd := string(req.Payload[4:])
- "%s@%s requested command %q",
- serverConn.RemoteAddr(),
- cmd, err := commands.Find(rawCmd, s.reposPath)
- log.Warnf("failed to find command for %q, %v", rawCmd, err)
+ if err := cmd.Run(session, username); err != nil { + "%s@%s command %q failed: %v", - if err := cmd.Run(conn, serverConn, username, req); err != nil {
- "%s@%s command %q failed: %v",
- serverConn.RemoteAddr(),
- "%s@%s command %q succeed",
- serverConn.RemoteAddr(),
+ if err := session.Exit(255); err != nil { + log.Errorf("session failed to exit: %v", err) + "%s@%s command %q succeed",
+ if err := session.Exit(0); err != nil { + log.Errorf("session failed to exit: %v", err) +func (s *Server) Listen(addr string) error {
- "%s@%s unsupported request: %q",
- serverConn.RemoteAddr(),
+ if err := s.server.ListenAndServe(); err != nil { + if err != ssh.ErrServerClosed {