grim/hgkeeper

Parents f97b7aa39a3d
Children 8f70636bb8a3
move to github.com/gliderlabs/ssh and the fall out from that, including that everything just works right now...
--- 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 @@
require (
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 @@
"fmt"
"github.com/alecthomas/kong"
+ "github.com/gliderlabs/ssh"
"github.com/kballard/go-shellquote"
- "golang.org/x/crypto/ssh"
)
type cli struct {
@@ -21,8 +21,7 @@
}
type Command interface {
- Run(conn ssh.Channel, serverConn *ssh.ServerConn, username string, req *ssh.Request) error
- String() string
+ 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 @@
"fmt"
"path/filepath"
- "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 {
- return "hg init"
-}
--- 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 @@
import (
"io"
- "strings"
"sync"
+ "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 {
cmd := hgCmd.Cmd()
if err := hgCmd.Setup(); err != nil {
return err
}
- teardown := func() {
- hgCmd.Teardown()
-
- if err := cmd.Wait(); err != nil {
- log.Warnf(
- "%s command %q failed: %v",
- serverConn.RemoteAddr(),
- strings.Join(cmd.Args, " "),
- err,
- )
-
- if req.WantReply {
- req.Reply(false, nil)
- }
- } else {
- log.Debugf(
- "%s command %s finished",
- serverConn.RemoteAddr(),
- strings.Join(cmd.Args, " "),
- )
-
- if req.WantReply {
- req.Reply(true, nil)
- }
- }
-
- conn.Close()
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return err
}
- stdinWriter, err := cmd.StdinPipe()
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ return err
+ }
+
+ stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
- stdoutReader, err := cmd.StdoutPipe()
- if err != nil {
- return err
- }
+ wg := &sync.WaitGroup{}
+ // we wait for stderr and stdout to finish
+ wg.Add(2)
- stderrReader, err := cmd.StderrPipe()
- if err != nil {
+ if err := cmd.Start(); err != nil {
return err
}
- var once sync.Once
-
- // now wire up stdin/stdout/stderr
go func() {
- io.Copy(stdinWriter, conn)
- once.Do(teardown)
+ defer stdin.Close()
+ if _, err := io.Copy(stdin, session); err != nil {
+ log.Errorf("Failed to read stdin from session: %v", err)
+ }
}()
go func() {
- io.Copy(conn, stdoutReader)
- once.Do(teardown)
+ defer wg.Done()
+ if _, err := io.Copy(session, stdout); err != nil {
+ log.Errorf("Failed to write stdout to session: %v", err)
+ }
}()
go func() {
- io.Copy(conn.Stderr(), stderrReader)
- once.Do(teardown)
+ defer wg.Done()
+ 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 {
- req.Reply(false, nil)
- once.Do(teardown)
- return err
- }
+ // wait until all output is processed
+ wg.Wait()
- return nil
+ return cmd.Wait()
}
--- 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 @@
"fmt"
"path/filepath"
+ "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 {
return err
}
@@ -42,7 +42,3 @@
return nil
}
-
-func (s *Serve) String() string {
- return "hg serve"
-}
--- 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 @@
continue
}
- s.cfg.AddHostKey(key)
+ s.server.AddHostKey(key)
found = true
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 @@
package ssh
import (
- "fmt"
- "net"
-
+ "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 @@
type Server struct {
reposPath string
- cfg *ssh.ServerConfig
- listener net.Listener
+ server *ssh.Server
}
func NewServer(hostKeysPath, reposPath, adminRepo string) (*Server, error) {
@@ -22,164 +19,100 @@
reposPath: reposPath,
}
- s.cfg = &ssh.ServerConfig{
- MaxAuthTries: 1,
- PublicKeyCallback: s.publicKeyCallback,
+ s.server = &ssh.Server{
+ PublicKeyHandler: s.publicKeyHandler,
+ Handler: s.sessionHandler,
+ PtyCallback: s.ptyCallback,
}
- var err error
- if err = s.setHostKeysPath(hostKeysPath); err != nil {
+ if err := s.setHostKeysPath(hostKeysPath); err != nil {
return nil, err
}
return s, 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 {
+ return false
+}
+
+func (s *Server) publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
+ username, err := access.UsernameFromPubkey(key)
if err != nil {
- return err
+ log.Warnf("authentication failure, unknown key %s", gossh.FingerprintSHA256(key))
+
+ return false
}
- s.listener = listener
-
- log.Infof("listening for ssh connections on %s", addr)
- for {
- tcpConn, err := s.listener.Accept()
- if err != nil {
- log.Errorf(
- "failed to accept ssh connection for %s: %v",
- tcpConn.RemoteAddr(),
- err,
- )
- continue
- }
-
- sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, s.cfg)
- if err != nil {
- log.Errorf(
- "ssh handshake failed for %q : %v",
- tcpConn.RemoteAddr(),
- err,
- )
- continue
- }
-
- log.Infof(
- "new ssh connection for %q from %s(%s)",
- sshConn.Permissions.Extensions["username"],
- sshConn.RemoteAddr(),
- 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)
- if err != nil {
- return nil, err
- }
+ ctx.SetValue("username", username)
log.Infof(
"%q authenticated with %s",
username,
- ssh.FingerprintSHA256(key),
+ gossh.FingerprintSHA256(key),
)
- return &ssh.Permissions{
- Extensions: map[string]string{"username": username},
- }, nil
+ return true
}
-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 {
return
}
- conn, requests, err := ch.Accept()
+ username := session.Context().Value("username").(string)
+
+ log.Infof(
+ "%s@%s requested command %q",
+ username,
+ session.RemoteAddr(),
+ session.RawCommand(),
+ )
+
+ cmd, err := commands.Find(session.RawCommand(), s.reposPath)
if err != nil {
- log.Warnf("failed to accept connection: %v", err)
+ log.Warnf("failed to find command for %q, %v", session.RawCommand(), err)
+
return
}
- // now run through all of the requests but only handle shell requests
- go func() {
- for req := range requests {
- username := serverConn.Permissions.Extensions["username"]
-
- switch req.Type {
- case "exec":
- // 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
- // works fine.
- rawCmd := string(req.Payload[4:])
-
- log.Infof(
- "%s@%s requested command %q",
- username,
- serverConn.RemoteAddr(),
- rawCmd,
- )
-
- cmd, err := commands.Find(rawCmd, s.reposPath)
- if err != nil {
- log.Warnf("failed to find command for %q, %v", rawCmd, err)
- req.Reply(false, nil)
-
- continue
- }
+ if err := cmd.Run(session, username); err != nil {
+ log.Warnf(
+ "%s@%s command %q failed: %v",
+ username,
+ session.RemoteAddr(),
+ session.RawCommand(),
+ err,
+ )
- if err := cmd.Run(conn, serverConn, username, req); err != nil {
- log.Warnf(
- "%s@%s command %q failed: %v",
- username,
- serverConn.RemoteAddr(),
- rawCmd,
- err,
- )
-
- if req.WantReply {
- req.Reply(false, nil)
- }
- } else {
- log.Infof(
- "%s@%s command %q succeed",
- username,
- serverConn.RemoteAddr(),
- rawCmd,
- )
+ if err := session.Exit(255); err != nil {
+ log.Errorf("session failed to exit: %v", err)
+ }
+ } else {
+ log.Infof(
+ "%s@%s command %q succeed",
+ username,
+ session.RemoteAddr(),
+ session.RawCommand(),
+ )
- if req.WantReply {
- req.Reply(true, nil)
- }
- }
+ if err := session.Exit(0); err != nil {
+ log.Errorf("session failed to exit: %v", err)
+ }
+ }
+}
+
+func (s *Server) Listen(addr string) error {
+ s.server.Addr = addr
- default:
- log.Debugf(
- "%s@%s unsupported request: %q",
- username,
- serverConn.RemoteAddr(),
- req.Type,
- )
+ if err := s.server.ListenAndServe(); err != nil {
+ if err != ssh.ErrServerClosed {
+ return err
+ }
+ }
- if req.WantReply {
- req.Reply(false, nil)
- }
- }
- }
- }()
+ return nil
}