From 30c9e2af9b5ca4c2ff2bb9f28291e251c380603c Mon Sep 17 00:00:00 2001 From: Austin Cherry Date: Thu, 18 Jul 2019 16:29:16 -0500 Subject: [PATCH] AUTH-1941: Adds initial SSH server implementation --- Gopkg.lock | 26 ++ Gopkg.toml | 8 + cmd/cloudflared/tunnel/cmd.go | 58 ++- sshserver/sshserver_unix.go | 170 ++++++++ sshserver/sshserver_windows.go | 17 + vendor/github.com/anmitsu/go-shlex/.gitignore | 1 + vendor/github.com/anmitsu/go-shlex/LICENSE | 20 + vendor/github.com/anmitsu/go-shlex/README.md | 38 ++ vendor/github.com/anmitsu/go-shlex/shlex.go | 193 +++++++++ vendor/github.com/creack/pty/.gitignore | 4 + vendor/github.com/creack/pty/Dockerfile.riscv | 14 + vendor/github.com/creack/pty/LICENSE | 23 + vendor/github.com/creack/pty/README.md | 100 +++++ vendor/github.com/creack/pty/doc.go | 16 + vendor/github.com/creack/pty/ioctl.go | 13 + vendor/github.com/creack/pty/ioctl_bsd.go | 39 ++ vendor/github.com/creack/pty/ioctl_solaris.go | 30 ++ vendor/github.com/creack/pty/mktypes.bash | 19 + vendor/github.com/creack/pty/pty_darwin.go | 65 +++ vendor/github.com/creack/pty/pty_dragonfly.go | 80 ++++ vendor/github.com/creack/pty/pty_freebsd.go | 78 ++++ vendor/github.com/creack/pty/pty_linux.go | 51 +++ vendor/github.com/creack/pty/pty_openbsd.go | 33 ++ vendor/github.com/creack/pty/pty_solaris.go | 139 ++++++ .../github.com/creack/pty/pty_unsupported.go | 11 + vendor/github.com/creack/pty/run.go | 57 +++ .../creack/pty/test_crosscompile.sh | 50 +++ vendor/github.com/creack/pty/types.go | 10 + .../github.com/creack/pty/types_dragonfly.go | 17 + vendor/github.com/creack/pty/types_freebsd.go | 15 + vendor/github.com/creack/pty/types_openbsd.go | 14 + vendor/github.com/creack/pty/util.go | 64 +++ vendor/github.com/creack/pty/util_solaris.go | 51 +++ vendor/github.com/creack/pty/ztypes_386.go | 9 + vendor/github.com/creack/pty/ztypes_amd64.go | 9 + vendor/github.com/creack/pty/ztypes_arm.go | 9 + vendor/github.com/creack/pty/ztypes_arm64.go | 11 + .../creack/pty/ztypes_dragonfly_amd64.go | 14 + .../creack/pty/ztypes_freebsd_386.go | 13 + .../creack/pty/ztypes_freebsd_amd64.go | 14 + .../creack/pty/ztypes_freebsd_arm.go | 13 + vendor/github.com/creack/pty/ztypes_mipsx.go | 12 + .../creack/pty/ztypes_openbsd_386.go | 13 + .../creack/pty/ztypes_openbsd_amd64.go | 13 + vendor/github.com/creack/pty/ztypes_ppc64.go | 11 + .../github.com/creack/pty/ztypes_ppc64le.go | 11 + vendor/github.com/creack/pty/ztypes_riscvx.go | 11 + vendor/github.com/creack/pty/ztypes_s390x.go | 11 + vendor/github.com/gliderlabs/ssh/LICENSE | 27 ++ vendor/github.com/gliderlabs/ssh/README.md | 96 +++++ vendor/github.com/gliderlabs/ssh/agent.go | 83 ++++ vendor/github.com/gliderlabs/ssh/circle.yml | 26 ++ vendor/github.com/gliderlabs/ssh/conn.go | 55 +++ vendor/github.com/gliderlabs/ssh/context.go | 152 +++++++ vendor/github.com/gliderlabs/ssh/doc.go | 45 ++ vendor/github.com/gliderlabs/ssh/options.go | 77 ++++ vendor/github.com/gliderlabs/ssh/server.go | 394 ++++++++++++++++++ vendor/github.com/gliderlabs/ssh/session.go | 308 ++++++++++++++ vendor/github.com/gliderlabs/ssh/ssh.go | 123 ++++++ vendor/github.com/gliderlabs/ssh/tcpip.go | 193 +++++++++ vendor/github.com/gliderlabs/ssh/util.go | 83 ++++ vendor/github.com/gliderlabs/ssh/wrap.go | 33 ++ 62 files changed, 3384 insertions(+), 9 deletions(-) create mode 100644 sshserver/sshserver_unix.go create mode 100644 sshserver/sshserver_windows.go create mode 100644 vendor/github.com/anmitsu/go-shlex/.gitignore create mode 100644 vendor/github.com/anmitsu/go-shlex/LICENSE create mode 100644 vendor/github.com/anmitsu/go-shlex/README.md create mode 100644 vendor/github.com/anmitsu/go-shlex/shlex.go create mode 100644 vendor/github.com/creack/pty/.gitignore create mode 100644 vendor/github.com/creack/pty/Dockerfile.riscv create mode 100644 vendor/github.com/creack/pty/LICENSE create mode 100644 vendor/github.com/creack/pty/README.md create mode 100644 vendor/github.com/creack/pty/doc.go create mode 100644 vendor/github.com/creack/pty/ioctl.go create mode 100644 vendor/github.com/creack/pty/ioctl_bsd.go create mode 100644 vendor/github.com/creack/pty/ioctl_solaris.go create mode 100755 vendor/github.com/creack/pty/mktypes.bash create mode 100644 vendor/github.com/creack/pty/pty_darwin.go create mode 100644 vendor/github.com/creack/pty/pty_dragonfly.go create mode 100644 vendor/github.com/creack/pty/pty_freebsd.go create mode 100644 vendor/github.com/creack/pty/pty_linux.go create mode 100644 vendor/github.com/creack/pty/pty_openbsd.go create mode 100644 vendor/github.com/creack/pty/pty_solaris.go create mode 100644 vendor/github.com/creack/pty/pty_unsupported.go create mode 100644 vendor/github.com/creack/pty/run.go create mode 100755 vendor/github.com/creack/pty/test_crosscompile.sh create mode 100644 vendor/github.com/creack/pty/types.go create mode 100644 vendor/github.com/creack/pty/types_dragonfly.go create mode 100644 vendor/github.com/creack/pty/types_freebsd.go create mode 100644 vendor/github.com/creack/pty/types_openbsd.go create mode 100644 vendor/github.com/creack/pty/util.go create mode 100644 vendor/github.com/creack/pty/util_solaris.go create mode 100644 vendor/github.com/creack/pty/ztypes_386.go create mode 100644 vendor/github.com/creack/pty/ztypes_amd64.go create mode 100644 vendor/github.com/creack/pty/ztypes_arm.go create mode 100644 vendor/github.com/creack/pty/ztypes_arm64.go create mode 100644 vendor/github.com/creack/pty/ztypes_dragonfly_amd64.go create mode 100644 vendor/github.com/creack/pty/ztypes_freebsd_386.go create mode 100644 vendor/github.com/creack/pty/ztypes_freebsd_amd64.go create mode 100644 vendor/github.com/creack/pty/ztypes_freebsd_arm.go create mode 100644 vendor/github.com/creack/pty/ztypes_mipsx.go create mode 100644 vendor/github.com/creack/pty/ztypes_openbsd_386.go create mode 100644 vendor/github.com/creack/pty/ztypes_openbsd_amd64.go create mode 100644 vendor/github.com/creack/pty/ztypes_ppc64.go create mode 100644 vendor/github.com/creack/pty/ztypes_ppc64le.go create mode 100644 vendor/github.com/creack/pty/ztypes_riscvx.go create mode 100644 vendor/github.com/creack/pty/ztypes_s390x.go create mode 100644 vendor/github.com/gliderlabs/ssh/LICENSE create mode 100644 vendor/github.com/gliderlabs/ssh/README.md create mode 100644 vendor/github.com/gliderlabs/ssh/agent.go create mode 100644 vendor/github.com/gliderlabs/ssh/circle.yml create mode 100644 vendor/github.com/gliderlabs/ssh/conn.go create mode 100644 vendor/github.com/gliderlabs/ssh/context.go create mode 100644 vendor/github.com/gliderlabs/ssh/doc.go create mode 100644 vendor/github.com/gliderlabs/ssh/options.go create mode 100644 vendor/github.com/gliderlabs/ssh/server.go create mode 100644 vendor/github.com/gliderlabs/ssh/session.go create mode 100644 vendor/github.com/gliderlabs/ssh/ssh.go create mode 100644 vendor/github.com/gliderlabs/ssh/tcpip.go create mode 100644 vendor/github.com/gliderlabs/ssh/util.go create mode 100644 vendor/github.com/gliderlabs/ssh/wrap.go diff --git a/Gopkg.lock b/Gopkg.lock index 87b1f74b..fb77aa73 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,14 @@ revision = "3f9954f6f6697845b082ca57995849ddf614f450" version = "v1.3.3" +[[projects]] + branch = "master" + digest = "1:1a200e7e73293b75eb8e5c93d023b5472663432da0b663e1532624fcfede9ca8" + name = "github.com/anmitsu/go-shlex" + packages = ["."] + pruneopts = "UT" + revision = "648efa622239a2f6ff949fed78ee37b48d499ba4" + [[projects]] digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d" name = "github.com/beorn7/perks" @@ -112,6 +120,14 @@ revision = "95778dfbb74eb7e4dbaf43bf7d71809650ef8076" version = "v19" +[[projects]] + digest = "1:385dff5bf83bf1c9d40ef2b0f2e697f0d1973fc9e24ae9401cf41698612bbebc" + name = "github.com/creack/pty" + packages = ["."] + pruneopts = "UT" + revision = "2769f65a3a94eb8f876f44a0459d24ae7ad2e488" + version = "v1.1.7" + [[projects]] digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" name = "github.com/davecgh/go-spew" @@ -177,6 +193,14 @@ pruneopts = "UT" revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa" +[[projects]] + digest = "1:0a01355005024757ae2a1f62e8fe68a30b4f5fde7a08ac88b3685e7999ac354b" + name = "github.com/gliderlabs/ssh" + packages = ["."] + pruneopts = "UT" + revision = "5b6cc7030f17095c0cf23bb063b0bfe824fe5f8b" + version = "v0.2.2" + [[projects]] digest = "1:ec6f9bf5e274c833c911923c9193867f3f18788c461f76f05f62bb1510e0ae65" name = "github.com/go-sql-driver/mysql" @@ -733,10 +757,12 @@ "github.com/coredns/coredns/request", "github.com/coreos/go-oidc", "github.com/coreos/go-systemd/daemon", + "github.com/creack/pty", "github.com/denisenkom/go-mssqldb", "github.com/equinox-io/equinox", "github.com/facebookgo/grace/gracenet", "github.com/getsentry/raven-go", + "github.com/gliderlabs/ssh", "github.com/go-sql-driver/mysql", "github.com/golang-collections/collections/queue", "github.com/google/uuid", diff --git a/Gopkg.toml b/Gopkg.toml index da88ce70..252b8690 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -115,3 +115,11 @@ [[constraint]] branch = "v2" name = "github.com/coreos/go-oidc" + +[[constraint]] + name = "github.com/gliderlabs/ssh" + version = "0.2.2" + +[[constraint]] + name = "github.com/creack/pty" + version = "1.1.7" diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 20409045..ca3e59ce 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -8,31 +8,32 @@ import ( "net/url" "os" "reflect" + "runtime" "runtime/trace" "sync" "time" - "github.com/cloudflare/cloudflared/dbconnect" - "github.com/cloudflare/cloudflared/tunnelrpc/pogs" - - "github.com/cloudflare/cloudflared/connection" - "github.com/cloudflare/cloudflared/supervisor" - "github.com/google/uuid" - - "github.com/getsentry/raven-go" - "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/cmd/cloudflared/config" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater" + "github.com/cloudflare/cloudflared/connection" + "github.com/cloudflare/cloudflared/dbconnect" "github.com/cloudflare/cloudflared/hello" "github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/signal" + "github.com/cloudflare/cloudflared/sshserver" + "github.com/cloudflare/cloudflared/supervisor" "github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tunneldns" + "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/websocket" + "github.com/coreos/go-systemd/daemon" "github.com/facebookgo/grace/gracenet" + "github.com/getsentry/raven-go" + "github.com/gliderlabs/ssh" + "github.com/google/uuid" "github.com/pkg/errors" "gopkg.in/urfave/cli.v2" "gopkg.in/urfave/cli.v2/altsrc" @@ -290,6 +291,31 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan c.Set("url", "https://"+helloListener.Addr().String()) } + if c.IsSet("ssh-server") { + if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { + logger.Errorf("--ssh-server is not supported on %s", runtime.GOOS) + return errors.New(fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS)) + + } + + logger.Infof("ssh-server set") + + sshServerAddress := "127.0.0.1:" + c.String("local-ssh-port") + server, err := sshserver.New(logger, sshServerAddress, shutdownC) + if err != nil { + logger.WithError(err).Error("Cannot create new SSH Server") + return errors.Wrap(err, "Cannot create new SSH Server") + } + wg.Add(1) + go func() { + defer wg.Done() + if err = server.Start(); err != nil && err != ssh.ErrServerClosed { + logger.WithError(err).Error("SSH server error") + } + }() + c.Set("url", "ssh://"+sshServerAddress) + } + if host := hostnameFromURI(c.String("url")); host != "" { listener, err := net.Listen("tcp", "127.0.0.1:") if err != nil { @@ -749,6 +775,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag { EnvVars: []string{"TUNNEL_HELLO_WORLD"}, Hidden: shouldHide, }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: "ssh-server", + Value: false, + Usage: "Run an SSH Server", + EnvVars: []string{"TUNNEL_SSH_SERVER"}, + Hidden: true, // TODO: remove when feature is complete + }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "pidfile", Usage: "Write the application's PID to this file after first successful connection.", @@ -891,5 +924,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag { EnvVars: []string{"DIAL_EDGE_TIMEOUT"}, Hidden: true, }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "local-ssh-port", + Usage: "Localhost port that cloudflared SSH server will run on", + Value: "22", + EnvVars: []string{"LOCAL_SSH_PORT"}, + Hidden: true, + }), } } diff --git a/sshserver/sshserver_unix.go b/sshserver/sshserver_unix.go new file mode 100644 index 00000000..91d68ebe --- /dev/null +++ b/sshserver/sshserver_unix.go @@ -0,0 +1,170 @@ +//+build !windows + +package sshserver + +import ( + "bufio" + "fmt" + "github.com/pkg/errors" + "io" + "os" + "os/exec" + "os/user" + "strconv" + "syscall" + "unsafe" + + "github.com/creack/pty" + "github.com/gliderlabs/ssh" + "github.com/sirupsen/logrus" +) + +type SSHServer struct { + ssh.Server + logger *logrus.Logger + shutdownC chan struct{} +} + +const DefaultShellPrompt = `\e[0;31m[\u@\h \W]\$ \e[m ` + +func New(logger *logrus.Logger, address string, shutdownC chan struct{}) (*SSHServer, error) { + currentUser, err := user.Current() + if err != nil { + return nil, err + } + if currentUser.Uid != "0" { + return nil, errors.New("cloudflared ssh server needs to run as root") + } + + sshServer := SSHServer{ssh.Server{Addr: address}, logger, shutdownC} + return &sshServer, nil +} + +func (s *SSHServer) Start() error { + s.logger.Infof("Starting SSH server at %s", s.Addr) + + go func() { + <-s.shutdownC + if err := s.Close(); err != nil { + s.logger.WithError(err).Error("Cannot close SSH server") + } + }() + + s.Handle(s.connectionHandler) + return s.ListenAndServe() +} + +func (s *SSHServer) connectionHandler(session ssh.Session) { + + // Get uid and gid of user attempting to login + uid, gid, err := getUser(session.User()) + if err != nil { + if _, err := io.WriteString(session, "Invalid credentials\n"); err != nil { + s.logger.WithError(err).Error("Invalid credentials: Failed to write to SSH session") + } + if err := session.Exit(1); err != nil { + s.logger.WithError(err).Error("Failed to close SSH session") + } + return + } + + // Spawn shell under user + cmd := exec.Command("/bin/bash") + cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uid, Gid: gid}} + + ptyReq, winCh, isPty := session.Pty() + if !isPty { + if _, err := io.WriteString(session, "No PTY requested.\n"); err != nil { + s.logger.WithError(err).Error("No PTY requested: Failed to write to SSH session") + } + + if err := session.Exit(1); err != nil { + s.logger.WithError(err).Error("Failed to close SSH session") + } + return + } + + cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) + cmd.Env = append(cmd.Env, fmt.Sprintf("PS1=%s", DefaultShellPrompt)) + psuedoTTY, err := pty.Start(cmd) + if err != nil { + s.logger.WithError(err).Error("Failed to start pty session") + if err := session.Exit(1); err != nil { + s.logger.WithError(err).Error("Failed to close SSH session") + } + close(s.shutdownC) + return + } + + // Handle terminal window size changes + go func() { + for win := range winCh { + if errNo := setWinsize(psuedoTTY, win.Width, win.Height); errNo != 0 { + s.logger.WithError(err).Error("Failed to set pty window size: ", err.Error()) + if err := session.Exit(1); err != nil { + s.logger.WithError(err).Error("Failed to close SSH session") + } + close(s.shutdownC) + return + } + } + }() + + // Write incoming commands to PTY + go func() { + if _, err := io.Copy(psuedoTTY, session); err != nil { + s.logger.WithError(err).Error("Failed to write incoming command to pty") + } + }() + pr, pw := io.Pipe() + scanner := bufio.NewScanner(pr) + go func() { + for scanner.Scan() { + s.logger.Info(scanner.Text()) + } + }() + + // Write outgoing command output to both the command recorder, and remote user + mw := io.MultiWriter(pw, session) + if _, err := io.Copy(mw, psuedoTTY); err != nil { + s.logger.WithError(err).Error("Failed to write command output to user") + } + + if err := pw.Close(); err != nil { + s.logger.WithError(err).Error("Failed to close pipe writer") + } + + if err := pr.Close(); err != nil { + s.logger.WithError(err).Error("Failed to close pipe reader") + } + + // Wait for all resources associated with cmd to be released + // Returns error if shell exited with a non-zero status or received a signal + if err := cmd.Wait(); err != nil { + s.logger.WithError(err).Debug("Shell did not close correctly") + } +} + +// Sets PTY window size for terminal +func setWinsize(f *os.File, w, h int) syscall.Errno { + _, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), + uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) + return errNo +} + +// Only works on POSIX systems +func getUser(username string) (uint32, uint32, error) { + sshUser, err := user.Lookup(username) + if err != nil { + return 0, 0, err + } + uid, err := strconv.ParseUint(sshUser.Uid, 10, 32) + if err != nil { + return 0, 0, err + } + gid, err := strconv.ParseUint(sshUser.Gid, 10, 32) + if err != nil { + return 0, 0, err + } + return uint32(uid), uint32(gid), nil +} diff --git a/sshserver/sshserver_windows.go b/sshserver/sshserver_windows.go new file mode 100644 index 00000000..e0780802 --- /dev/null +++ b/sshserver/sshserver_windows.go @@ -0,0 +1,17 @@ +//+build windows + +package sshserver + +import ( + "github.com/sirupsen/logrus" +) + +type SSHServer struct{} + +func New(_ *logrus.Logger, _ string, _ chan struct{}) (*SSHServer, error) { + return nil, nil +} + +func (s *SSHServer) Start() error { + return nil +} diff --git a/vendor/github.com/anmitsu/go-shlex/.gitignore b/vendor/github.com/anmitsu/go-shlex/.gitignore new file mode 100644 index 00000000..0b46bb5d --- /dev/null +++ b/vendor/github.com/anmitsu/go-shlex/.gitignore @@ -0,0 +1 @@ +shlex.test diff --git a/vendor/github.com/anmitsu/go-shlex/LICENSE b/vendor/github.com/anmitsu/go-shlex/LICENSE new file mode 100644 index 00000000..4a17268a --- /dev/null +++ b/vendor/github.com/anmitsu/go-shlex/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) anmitsu + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/anmitsu/go-shlex/README.md b/vendor/github.com/anmitsu/go-shlex/README.md new file mode 100644 index 00000000..c4ffe72b --- /dev/null +++ b/vendor/github.com/anmitsu/go-shlex/README.md @@ -0,0 +1,38 @@ +# go-shlex + +go-shlex is a library to make a lexical analyzer like Unix shell for +Go. + +## Install + + go get -u "github.com/anmitsu/go-shlex" + +## Usage + +```go +package main + +import ( + "fmt" + "log" + + "github.com/anmitsu/go-shlex" +) + +func main() { + cmd := `cp -Rdp "file name" 'file name2' dir\ name` + words, err := shlex.Split(cmd, true) + if err != nil { + log.Fatal(err) + } + + for _, w := range words { + fmt.Println(w) + } +} +``` + +## Documentation + +http://godoc.org/github.com/anmitsu/go-shlex + diff --git a/vendor/github.com/anmitsu/go-shlex/shlex.go b/vendor/github.com/anmitsu/go-shlex/shlex.go new file mode 100644 index 00000000..e742c38a --- /dev/null +++ b/vendor/github.com/anmitsu/go-shlex/shlex.go @@ -0,0 +1,193 @@ +// Package shlex provides a simple lexical analysis like Unix shell. +package shlex + +import ( + "bufio" + "errors" + "io" + "strings" + "unicode" +) + +var ( + ErrNoClosing = errors.New("No closing quotation") + ErrNoEscaped = errors.New("No escaped character") +) + +// Tokenizer is the interface that classifies a token according to +// words, whitespaces, quotations, escapes and escaped quotations. +type Tokenizer interface { + IsWord(rune) bool + IsWhitespace(rune) bool + IsQuote(rune) bool + IsEscape(rune) bool + IsEscapedQuote(rune) bool +} + +// DefaultTokenizer implements a simple tokenizer like Unix shell. +type DefaultTokenizer struct{} + +func (t *DefaultTokenizer) IsWord(r rune) bool { + return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r) +} +func (t *DefaultTokenizer) IsQuote(r rune) bool { + switch r { + case '\'', '"': + return true + default: + return false + } +} +func (t *DefaultTokenizer) IsWhitespace(r rune) bool { + return unicode.IsSpace(r) +} +func (t *DefaultTokenizer) IsEscape(r rune) bool { + return r == '\\' +} +func (t *DefaultTokenizer) IsEscapedQuote(r rune) bool { + return r == '"' +} + +// Lexer represents a lexical analyzer. +type Lexer struct { + reader *bufio.Reader + tokenizer Tokenizer + posix bool + whitespacesplit bool +} + +// NewLexer creates a new Lexer reading from io.Reader. This Lexer +// has a DefaultTokenizer according to posix and whitespacesplit +// rules. +func NewLexer(r io.Reader, posix, whitespacesplit bool) *Lexer { + return &Lexer{ + reader: bufio.NewReader(r), + tokenizer: &DefaultTokenizer{}, + posix: posix, + whitespacesplit: whitespacesplit, + } +} + +// NewLexerString creates a new Lexer reading from a string. This +// Lexer has a DefaultTokenizer according to posix and whitespacesplit +// rules. +func NewLexerString(s string, posix, whitespacesplit bool) *Lexer { + return NewLexer(strings.NewReader(s), posix, whitespacesplit) +} + +// Split splits a string according to posix or non-posix rules. +func Split(s string, posix bool) ([]string, error) { + return NewLexerString(s, posix, true).Split() +} + +// SetTokenizer sets a Tokenizer. +func (l *Lexer) SetTokenizer(t Tokenizer) { + l.tokenizer = t +} + +func (l *Lexer) Split() ([]string, error) { + result := make([]string, 0) + for { + token, err := l.readToken() + if token != "" { + result = append(result, token) + } + + if err == io.EOF { + break + } else if err != nil { + return result, err + } + } + return result, nil +} + +func (l *Lexer) readToken() (string, error) { + t := l.tokenizer + token := "" + quoted := false + state := ' ' + escapedstate := ' ' +scanning: + for { + next, _, err := l.reader.ReadRune() + if err != nil { + if t.IsQuote(state) { + return token, ErrNoClosing + } else if t.IsEscape(state) { + return token, ErrNoEscaped + } + return token, err + } + + switch { + case t.IsWhitespace(state): + switch { + case t.IsWhitespace(next): + break scanning + case l.posix && t.IsEscape(next): + escapedstate = 'a' + state = next + case t.IsWord(next): + token += string(next) + state = 'a' + case t.IsQuote(next): + if !l.posix { + token += string(next) + } + state = next + default: + token = string(next) + if l.whitespacesplit { + state = 'a' + } else if token != "" || (l.posix && quoted) { + break scanning + } + } + case t.IsQuote(state): + quoted = true + switch { + case next == state: + if !l.posix { + token += string(next) + break scanning + } else { + state = 'a' + } + case l.posix && t.IsEscape(next) && t.IsEscapedQuote(state): + escapedstate = state + state = next + default: + token += string(next) + } + case t.IsEscape(state): + if t.IsQuote(escapedstate) && next != state && next != escapedstate { + token += string(state) + } + token += string(next) + state = escapedstate + case t.IsWord(state): + switch { + case t.IsWhitespace(next): + if token != "" || (l.posix && quoted) { + break scanning + } + case l.posix && t.IsQuote(next): + state = next + case l.posix && t.IsEscape(next): + escapedstate = 'a' + state = next + case t.IsWord(next) || t.IsQuote(next): + token += string(next) + default: + if l.whitespacesplit { + token += string(next) + } else if token != "" { + l.reader.UnreadRune() + break scanning + } + } + } + } + return token, nil +} diff --git a/vendor/github.com/creack/pty/.gitignore b/vendor/github.com/creack/pty/.gitignore new file mode 100644 index 00000000..1f0a99f2 --- /dev/null +++ b/vendor/github.com/creack/pty/.gitignore @@ -0,0 +1,4 @@ +[568].out +_go* +_test* +_obj diff --git a/vendor/github.com/creack/pty/Dockerfile.riscv b/vendor/github.com/creack/pty/Dockerfile.riscv new file mode 100644 index 00000000..476d8f11 --- /dev/null +++ b/vendor/github.com/creack/pty/Dockerfile.riscv @@ -0,0 +1,14 @@ +FROM golang:1.12 + +# Clone and complie a riscv compatible version of the go compiler. +RUN git clone https://review.gerrithub.io/riscv/riscv-go /riscv-go +# riscvdev branch HEAD as of 2019-06-29. +RUN cd /riscv-go && git checkout 04885fddd096d09d4450726064d06dd107e374bf +ENV PATH=/riscv-go/misc/riscv:/riscv-go/bin:$PATH +RUN cd /riscv-go/src && GOROOT_BOOTSTRAP=$(go env GOROOT) ./make.bash +ENV GOROOT=/riscv-go + +# Make sure we compile. +WORKDIR pty +ADD . . +RUN GOOS=linux GOARCH=riscv go build diff --git a/vendor/github.com/creack/pty/LICENSE b/vendor/github.com/creack/pty/LICENSE new file mode 100644 index 00000000..6b7558b6 --- /dev/null +++ b/vendor/github.com/creack/pty/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2011 Keith Rarick + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall +be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/creack/pty/README.md b/vendor/github.com/creack/pty/README.md new file mode 100644 index 00000000..5275014a --- /dev/null +++ b/vendor/github.com/creack/pty/README.md @@ -0,0 +1,100 @@ +# pty + +Pty is a Go package for using unix pseudo-terminals. + +## Install + + go get github.com/creack/pty + +## Example + +### Command + +```go +package main + +import ( + "github.com/creack/pty" + "io" + "os" + "os/exec" +) + +func main() { + c := exec.Command("grep", "--color=auto", "bar") + f, err := pty.Start(c) + if err != nil { + panic(err) + } + + go func() { + f.Write([]byte("foo\n")) + f.Write([]byte("bar\n")) + f.Write([]byte("baz\n")) + f.Write([]byte{4}) // EOT + }() + io.Copy(os.Stdout, f) +} +``` + +### Shell + +```go +package main + +import ( + "io" + "log" + "os" + "os/exec" + "os/signal" + "syscall" + + "github.com/creack/pty" + "golang.org/x/crypto/ssh/terminal" +) + +func test() error { + // Create arbitrary command. + c := exec.Command("bash") + + // Start the command with a pty. + ptmx, err := pty.Start(c) + if err != nil { + return err + } + // Make sure to close the pty at the end. + defer func() { _ = ptmx.Close() }() // Best effort. + + // Handle pty size. + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGWINCH) + go func() { + for range ch { + if err := pty.InheritSize(os.Stdin, ptmx); err != nil { + log.Printf("error resizing pty: %s", err) + } + } + }() + ch <- syscall.SIGWINCH // Initial resize. + + // Set stdin in raw mode. + oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + defer func() { _ = terminal.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. + + // Copy stdin to the pty and the pty to stdout. + go func() { _, _ = io.Copy(ptmx, os.Stdin) }() + _, _ = io.Copy(os.Stdout, ptmx) + + return nil +} + +func main() { + if err := test(); err != nil { + log.Fatal(err) + } +} +``` diff --git a/vendor/github.com/creack/pty/doc.go b/vendor/github.com/creack/pty/doc.go new file mode 100644 index 00000000..190cfbea --- /dev/null +++ b/vendor/github.com/creack/pty/doc.go @@ -0,0 +1,16 @@ +// Package pty provides functions for working with Unix terminals. +package pty + +import ( + "errors" + "os" +) + +// ErrUnsupported is returned if a function is not +// available on the current platform. +var ErrUnsupported = errors.New("unsupported") + +// Opens a pty and its corresponding tty. +func Open() (pty, tty *os.File, err error) { + return open() +} diff --git a/vendor/github.com/creack/pty/ioctl.go b/vendor/github.com/creack/pty/ioctl.go new file mode 100644 index 00000000..c85cdcd1 --- /dev/null +++ b/vendor/github.com/creack/pty/ioctl.go @@ -0,0 +1,13 @@ +// +build !windows,!solaris + +package pty + +import "syscall" + +func ioctl(fd, cmd, ptr uintptr) error { + _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr) + if e != 0 { + return e + } + return nil +} diff --git a/vendor/github.com/creack/pty/ioctl_bsd.go b/vendor/github.com/creack/pty/ioctl_bsd.go new file mode 100644 index 00000000..73b12c53 --- /dev/null +++ b/vendor/github.com/creack/pty/ioctl_bsd.go @@ -0,0 +1,39 @@ +// +build darwin dragonfly freebsd netbsd openbsd + +package pty + +// from +const ( + _IOC_VOID uintptr = 0x20000000 + _IOC_OUT uintptr = 0x40000000 + _IOC_IN uintptr = 0x80000000 + _IOC_IN_OUT uintptr = _IOC_OUT | _IOC_IN + _IOC_DIRMASK = _IOC_VOID | _IOC_OUT | _IOC_IN + + _IOC_PARAM_SHIFT = 13 + _IOC_PARAM_MASK = (1 << _IOC_PARAM_SHIFT) - 1 +) + +func _IOC_PARM_LEN(ioctl uintptr) uintptr { + return (ioctl >> 16) & _IOC_PARAM_MASK +} + +func _IOC(inout uintptr, group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return inout | (param_len&_IOC_PARAM_MASK)<<16 | uintptr(group)<<8 | ioctl_num +} + +func _IO(group byte, ioctl_num uintptr) uintptr { + return _IOC(_IOC_VOID, group, ioctl_num, 0) +} + +func _IOR(group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return _IOC(_IOC_OUT, group, ioctl_num, param_len) +} + +func _IOW(group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return _IOC(_IOC_IN, group, ioctl_num, param_len) +} + +func _IOWR(group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return _IOC(_IOC_IN_OUT, group, ioctl_num, param_len) +} diff --git a/vendor/github.com/creack/pty/ioctl_solaris.go b/vendor/github.com/creack/pty/ioctl_solaris.go new file mode 100644 index 00000000..f63985f3 --- /dev/null +++ b/vendor/github.com/creack/pty/ioctl_solaris.go @@ -0,0 +1,30 @@ +package pty + +import ( + "golang.org/x/sys/unix" + "unsafe" +) + +const ( + // see /usr/include/sys/stropts.h + I_PUSH = uintptr((int32('S')<<8 | 002)) + I_STR = uintptr((int32('S')<<8 | 010)) + I_FIND = uintptr((int32('S')<<8 | 013)) + // see /usr/include/sys/ptms.h + ISPTM = (int32('P') << 8) | 1 + UNLKPT = (int32('P') << 8) | 2 + PTSSTTY = (int32('P') << 8) | 3 + ZONEPT = (int32('P') << 8) | 4 + OWNERPT = (int32('P') << 8) | 5 +) + +type strioctl struct { + ic_cmd int32 + ic_timout int32 + ic_len int32 + ic_dp unsafe.Pointer +} + +func ioctl(fd, cmd, ptr uintptr) error { + return unix.IoctlSetInt(int(fd), uint(cmd), int(ptr)) +} diff --git a/vendor/github.com/creack/pty/mktypes.bash b/vendor/github.com/creack/pty/mktypes.bash new file mode 100755 index 00000000..82ee1672 --- /dev/null +++ b/vendor/github.com/creack/pty/mktypes.bash @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +GOOSARCH="${GOOS}_${GOARCH}" +case "$GOOSARCH" in +_* | *_ | _) + echo 'undefined $GOOS_$GOARCH:' "$GOOSARCH" 1>&2 + exit 1 + ;; +esac + +GODEFS="go tool cgo -godefs" + +$GODEFS types.go |gofmt > ztypes_$GOARCH.go + +case $GOOS in +freebsd|dragonfly|openbsd) + $GODEFS types_$GOOS.go |gofmt > ztypes_$GOOSARCH.go + ;; +esac diff --git a/vendor/github.com/creack/pty/pty_darwin.go b/vendor/github.com/creack/pty/pty_darwin.go new file mode 100644 index 00000000..6344b6b0 --- /dev/null +++ b/vendor/github.com/creack/pty/pty_darwin.go @@ -0,0 +1,65 @@ +package pty + +import ( + "errors" + "os" + "syscall" + "unsafe" +) + +func open() (pty, tty *os.File, err error) { + pFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC, 0) + if err != nil { + return nil, nil, err + } + p := os.NewFile(uintptr(pFD), "/dev/ptmx") + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + if err := grantpt(p); err != nil { + return nil, nil, err + } + + if err := unlockpt(p); err != nil { + return nil, nil, err + } + + t, err := os.OpenFile(sname, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func ptsname(f *os.File) (string, error) { + n := make([]byte, _IOC_PARM_LEN(syscall.TIOCPTYGNAME)) + + err := ioctl(f.Fd(), syscall.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n[0]))) + if err != nil { + return "", err + } + + for i, c := range n { + if c == 0 { + return string(n[:i]), nil + } + } + return "", errors.New("TIOCPTYGNAME string not NUL-terminated") +} + +func grantpt(f *os.File) error { + return ioctl(f.Fd(), syscall.TIOCPTYGRANT, 0) +} + +func unlockpt(f *os.File) error { + return ioctl(f.Fd(), syscall.TIOCPTYUNLK, 0) +} diff --git a/vendor/github.com/creack/pty/pty_dragonfly.go b/vendor/github.com/creack/pty/pty_dragonfly.go new file mode 100644 index 00000000..b7d1f20f --- /dev/null +++ b/vendor/github.com/creack/pty/pty_dragonfly.go @@ -0,0 +1,80 @@ +package pty + +import ( + "errors" + "os" + "strings" + "syscall" + "unsafe" +) + +// same code as pty_darwin.go +func open() (pty, tty *os.File, err error) { + p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + if err := grantpt(p); err != nil { + return nil, nil, err + } + + if err := unlockpt(p); err != nil { + return nil, nil, err + } + + t, err := os.OpenFile(sname, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func grantpt(f *os.File) error { + _, err := isptmaster(f.Fd()) + return err +} + +func unlockpt(f *os.File) error { + _, err := isptmaster(f.Fd()) + return err +} + +func isptmaster(fd uintptr) (bool, error) { + err := ioctl(fd, syscall.TIOCISPTMASTER, 0) + return err == nil, err +} + +var ( + emptyFiodgnameArg fiodgnameArg + ioctl_FIODNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg)) +) + +func ptsname(f *os.File) (string, error) { + name := make([]byte, _C_SPECNAMELEN) + fa := fiodgnameArg{Name: (*byte)(unsafe.Pointer(&name[0])), Len: _C_SPECNAMELEN, Pad_cgo_0: [4]byte{0, 0, 0, 0}} + + err := ioctl(f.Fd(), ioctl_FIODNAME, uintptr(unsafe.Pointer(&fa))) + if err != nil { + return "", err + } + + for i, c := range name { + if c == 0 { + s := "/dev/" + string(name[:i]) + return strings.Replace(s, "ptm", "pts", -1), nil + } + } + return "", errors.New("TIOCPTYGNAME string not NUL-terminated") +} diff --git a/vendor/github.com/creack/pty/pty_freebsd.go b/vendor/github.com/creack/pty/pty_freebsd.go new file mode 100644 index 00000000..63b6d913 --- /dev/null +++ b/vendor/github.com/creack/pty/pty_freebsd.go @@ -0,0 +1,78 @@ +package pty + +import ( + "errors" + "os" + "syscall" + "unsafe" +) + +func posixOpenpt(oflag int) (fd int, err error) { + r0, _, e1 := syscall.Syscall(syscall.SYS_POSIX_OPENPT, uintptr(oflag), 0, 0) + fd = int(r0) + if e1 != 0 { + err = e1 + } + return fd, err +} + +func open() (pty, tty *os.File, err error) { + fd, err := posixOpenpt(syscall.O_RDWR | syscall.O_CLOEXEC) + if err != nil { + return nil, nil, err + } + p := os.NewFile(uintptr(fd), "/dev/pts") + // In case of error after this point, make sure we close the pts fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + t, err := os.OpenFile("/dev/"+sname, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func isptmaster(fd uintptr) (bool, error) { + err := ioctl(fd, syscall.TIOCPTMASTER, 0) + return err == nil, err +} + +var ( + emptyFiodgnameArg fiodgnameArg + ioctlFIODGNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg)) +) + +func ptsname(f *os.File) (string, error) { + master, err := isptmaster(f.Fd()) + if err != nil { + return "", err + } + if !master { + return "", syscall.EINVAL + } + + const n = _C_SPECNAMELEN + 1 + var ( + buf = make([]byte, n) + arg = fiodgnameArg{Len: n, Buf: (*byte)(unsafe.Pointer(&buf[0]))} + ) + if err := ioctl(f.Fd(), ioctlFIODGNAME, uintptr(unsafe.Pointer(&arg))); err != nil { + return "", err + } + + for i, c := range buf { + if c == 0 { + return string(buf[:i]), nil + } + } + return "", errors.New("FIODGNAME string not NUL-terminated") +} diff --git a/vendor/github.com/creack/pty/pty_linux.go b/vendor/github.com/creack/pty/pty_linux.go new file mode 100644 index 00000000..4a833de1 --- /dev/null +++ b/vendor/github.com/creack/pty/pty_linux.go @@ -0,0 +1,51 @@ +package pty + +import ( + "os" + "strconv" + "syscall" + "unsafe" +) + +func open() (pty, tty *os.File, err error) { + p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + if err := unlockpt(p); err != nil { + return nil, nil, err + } + + t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func ptsname(f *os.File) (string, error) { + var n _C_uint + err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))) + if err != nil { + return "", err + } + return "/dev/pts/" + strconv.Itoa(int(n)), nil +} + +func unlockpt(f *os.File) error { + var u _C_int + // use TIOCSPTLCK with a pointer to zero to clear the lock + return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) +} diff --git a/vendor/github.com/creack/pty/pty_openbsd.go b/vendor/github.com/creack/pty/pty_openbsd.go new file mode 100644 index 00000000..a6a35d1e --- /dev/null +++ b/vendor/github.com/creack/pty/pty_openbsd.go @@ -0,0 +1,33 @@ +package pty + +import ( + "os" + "syscall" + "unsafe" +) + +func open() (pty, tty *os.File, err error) { + /* + * from ptm(4): + * The PTMGET command allocates a free pseudo terminal, changes its + * ownership to the caller, revokes the access privileges for all previous + * users, opens the file descriptors for the pty and tty devices and + * returns them to the caller in struct ptmget. + */ + + p, err := os.OpenFile("/dev/ptm", os.O_RDWR|syscall.O_CLOEXEC, 0) + if err != nil { + return nil, nil, err + } + defer p.Close() + + var ptm ptmget + if err := ioctl(p.Fd(), uintptr(ioctl_PTMGET), uintptr(unsafe.Pointer(&ptm))); err != nil { + return nil, nil, err + } + + pty = os.NewFile(uintptr(ptm.Cfd), "/dev/ptm") + tty = os.NewFile(uintptr(ptm.Sfd), "/dev/ptm") + + return pty, tty, nil +} diff --git a/vendor/github.com/creack/pty/pty_solaris.go b/vendor/github.com/creack/pty/pty_solaris.go new file mode 100644 index 00000000..09ec1b79 --- /dev/null +++ b/vendor/github.com/creack/pty/pty_solaris.go @@ -0,0 +1,139 @@ +package pty + +/* based on: +http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/pt.c +*/ + +import ( + "errors" + "golang.org/x/sys/unix" + "os" + "strconv" + "syscall" + "unsafe" +) + +const NODEV = ^uint64(0) + +func open() (pty, tty *os.File, err error) { + masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|unix.O_NOCTTY, 0) + //masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC|unix.O_NOCTTY, 0) + if err != nil { + return nil, nil, err + } + p := os.NewFile(uintptr(masterfd), "/dev/ptmx") + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + err = grantpt(p) + if err != nil { + return nil, nil, err + } + + err = unlockpt(p) + if err != nil { + return nil, nil, err + } + + slavefd, err := syscall.Open(sname, os.O_RDWR|unix.O_NOCTTY, 0) + if err != nil { + return nil, nil, err + } + t := os.NewFile(uintptr(slavefd), sname) + + // pushing terminal driver STREAMS modules as per pts(7) + for _, mod := range([]string{"ptem", "ldterm", "ttcompat"}) { + err = streams_push(t, mod) + if err != nil { + return nil, nil, err + } + } + + return p, t, nil +} + +func minor(x uint64) uint64 { + return x & 0377 +} + +func ptsdev(fd uintptr) uint64 { + istr := strioctl{ISPTM, 0, 0, nil} + err := ioctl(fd, I_STR, uintptr(unsafe.Pointer(&istr))) + if err != nil { + return NODEV + } + var status unix.Stat_t + err = unix.Fstat(int(fd), &status) + if err != nil { + return NODEV + } + return uint64(minor(status.Rdev)) +} + +func ptsname(f *os.File) (string, error) { + dev := ptsdev(f.Fd()) + if dev == NODEV { + return "", errors.New("not a master pty") + } + fn := "/dev/pts/" + strconv.FormatInt(int64(dev), 10) + // access(2) creates the slave device (if the pty exists) + // F_OK == 0 (unistd.h) + err := unix.Access(fn, 0) + if err != nil { + return "", err + } + return fn, nil +} + +type pt_own struct { + pto_ruid int32 + pto_rgid int32 +} + +func grantpt(f *os.File) error { + if ptsdev(f.Fd()) == NODEV { + return errors.New("not a master pty") + } + var pto pt_own + pto.pto_ruid = int32(os.Getuid()) + // XXX should first attempt to get gid of DEFAULT_TTY_GROUP="tty" + pto.pto_rgid = int32(os.Getgid()) + var istr strioctl + istr.ic_cmd = OWNERPT + istr.ic_timout = 0 + istr.ic_len = int32(unsafe.Sizeof(istr)) + istr.ic_dp = unsafe.Pointer(&pto) + err := ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr))) + if err != nil { + return errors.New("access denied") + } + return nil +} + +func unlockpt(f *os.File) error { + istr := strioctl{UNLKPT, 0, 0, nil} + return ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr))) +} + +// push STREAMS modules if not already done so +func streams_push(f *os.File, mod string) error { + var err error + buf := []byte(mod) + // XXX I_FIND is not returning an error when the module + // is already pushed even though truss reports a return + // value of 1. A bug in the Go Solaris syscall interface? + // XXX without this we are at risk of the issue + // https://www.illumos.org/issues/9042 + // but since we are not using libc or XPG4.2, we should not be + // double-pushing modules + + err = ioctl(f.Fd(), I_FIND, uintptr(unsafe.Pointer(&buf[0]))) + if err != nil { + return nil + } + err = ioctl(f.Fd(), I_PUSH, uintptr(unsafe.Pointer(&buf[0]))) + return err +} diff --git a/vendor/github.com/creack/pty/pty_unsupported.go b/vendor/github.com/creack/pty/pty_unsupported.go new file mode 100644 index 00000000..ceb425b1 --- /dev/null +++ b/vendor/github.com/creack/pty/pty_unsupported.go @@ -0,0 +1,11 @@ +// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd,!solaris + +package pty + +import ( + "os" +) + +func open() (pty, tty *os.File, err error) { + return nil, nil, ErrUnsupported +} diff --git a/vendor/github.com/creack/pty/run.go b/vendor/github.com/creack/pty/run.go new file mode 100644 index 00000000..959be26b --- /dev/null +++ b/vendor/github.com/creack/pty/run.go @@ -0,0 +1,57 @@ +// +build !windows + +package pty + +import ( + "os" + "os/exec" + "syscall" +) + +// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout, +// and c.Stderr, calls c.Start, and returns the File of the tty's +// corresponding pty. +func Start(c *exec.Cmd) (pty *os.File, err error) { + return StartWithSize(c, nil) +} + +// StartWithSize assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout, +// and c.Stderr, calls c.Start, and returns the File of the tty's +// corresponding pty. +// +// This will resize the pty to the specified size before starting the command +func StartWithSize(c *exec.Cmd, sz *Winsize) (pty *os.File, err error) { + pty, tty, err := Open() + if err != nil { + return nil, err + } + defer tty.Close() + if sz != nil { + err = Setsize(pty, sz) + if err != nil { + pty.Close() + return nil, err + } + } + if c.Stdout == nil { + c.Stdout = tty + } + if c.Stderr == nil { + c.Stderr = tty + } + if c.Stdin == nil { + c.Stdin = tty + } + if c.SysProcAttr == nil { + c.SysProcAttr = &syscall.SysProcAttr{} + } + c.SysProcAttr.Setctty = true + c.SysProcAttr.Setsid = true + c.SysProcAttr.Ctty = int(tty.Fd()) + err = c.Start() + if err != nil { + pty.Close() + return nil, err + } + return pty, err +} diff --git a/vendor/github.com/creack/pty/test_crosscompile.sh b/vendor/github.com/creack/pty/test_crosscompile.sh new file mode 100755 index 00000000..f0b1dcac --- /dev/null +++ b/vendor/github.com/creack/pty/test_crosscompile.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env sh + +# Test script checking that all expected os/arch compile properly. +# Does not actually test the logic, just the compilation so we make sure we don't break code depending on the lib. + +echo2() { + echo $@ >&2 +} + +trap end 0 +end() { + [ "$?" = 0 ] && echo2 "Pass." || (echo2 "Fail."; exit 1) +} + +cross() { + os=$1 + shift + echo2 "Build for $os." + for arch in $@; do + echo2 " - $os/$arch" + GOOS=$os GOARCH=$arch go build + done + echo2 +} + +set -e + +cross linux amd64 386 arm arm64 ppc64 ppc64le s390x mips mipsle mips64 mips64le +cross darwin amd64 386 arm arm64 +cross freebsd amd64 386 arm +cross netbsd amd64 386 arm +cross openbsd amd64 386 +cross dragonfly amd64 +cross solaris amd64 + +# Not expected to work but should still compile. +cross windows amd64 386 arm + +# TODO: Fix compilation error on openbsd/arm. +# TODO: Merge the solaris PR. + +# Some os/arch require a different compiler. Run in docker. +if ! hash docker; then + # If docker is not present, stop here. + return +fi + +echo2 "Build for linux." +echo2 " - linux/riscv" +docker build -t test -f Dockerfile.riscv . diff --git a/vendor/github.com/creack/pty/types.go b/vendor/github.com/creack/pty/types.go new file mode 100644 index 00000000..5aecb6bc --- /dev/null +++ b/vendor/github.com/creack/pty/types.go @@ -0,0 +1,10 @@ +// +build ignore + +package pty + +import "C" + +type ( + _C_int C.int + _C_uint C.uint +) diff --git a/vendor/github.com/creack/pty/types_dragonfly.go b/vendor/github.com/creack/pty/types_dragonfly.go new file mode 100644 index 00000000..5c0493b8 --- /dev/null +++ b/vendor/github.com/creack/pty/types_dragonfly.go @@ -0,0 +1,17 @@ +// +build ignore + +package pty + +/* +#define _KERNEL +#include +#include +#include +*/ +import "C" + +const ( + _C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */ +) + +type fiodgnameArg C.struct_fiodname_args diff --git a/vendor/github.com/creack/pty/types_freebsd.go b/vendor/github.com/creack/pty/types_freebsd.go new file mode 100644 index 00000000..ce3eb951 --- /dev/null +++ b/vendor/github.com/creack/pty/types_freebsd.go @@ -0,0 +1,15 @@ +// +build ignore + +package pty + +/* +#include +#include +*/ +import "C" + +const ( + _C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */ +) + +type fiodgnameArg C.struct_fiodgname_arg diff --git a/vendor/github.com/creack/pty/types_openbsd.go b/vendor/github.com/creack/pty/types_openbsd.go new file mode 100644 index 00000000..47701b5f --- /dev/null +++ b/vendor/github.com/creack/pty/types_openbsd.go @@ -0,0 +1,14 @@ +// +build ignore + +package pty + +/* +#include +#include +#include +*/ +import "C" + +type ptmget C.struct_ptmget + +var ioctl_PTMGET = C.PTMGET diff --git a/vendor/github.com/creack/pty/util.go b/vendor/github.com/creack/pty/util.go new file mode 100644 index 00000000..8fdde0ba --- /dev/null +++ b/vendor/github.com/creack/pty/util.go @@ -0,0 +1,64 @@ +// +build !windows,!solaris + +package pty + +import ( + "os" + "syscall" + "unsafe" +) + +// InheritSize applies the terminal size of pty to tty. This should be run +// in a signal handler for syscall.SIGWINCH to automatically resize the tty when +// the pty receives a window size change notification. +func InheritSize(pty, tty *os.File) error { + size, err := GetsizeFull(pty) + if err != nil { + return err + } + err = Setsize(tty, size) + if err != nil { + return err + } + return nil +} + +// Setsize resizes t to s. +func Setsize(t *os.File, ws *Winsize) error { + return windowRectCall(ws, t.Fd(), syscall.TIOCSWINSZ) +} + +// GetsizeFull returns the full terminal size description. +func GetsizeFull(t *os.File) (size *Winsize, err error) { + var ws Winsize + err = windowRectCall(&ws, t.Fd(), syscall.TIOCGWINSZ) + return &ws, err +} + +// Getsize returns the number of rows (lines) and cols (positions +// in each line) in terminal t. +func Getsize(t *os.File) (rows, cols int, err error) { + ws, err := GetsizeFull(t) + return int(ws.Rows), int(ws.Cols), err +} + +// Winsize describes the terminal size. +type Winsize struct { + Rows uint16 // ws_row: Number of rows (in cells) + Cols uint16 // ws_col: Number of columns (in cells) + X uint16 // ws_xpixel: Width in pixels + Y uint16 // ws_ypixel: Height in pixels +} + +func windowRectCall(ws *Winsize, fd, a2 uintptr) error { + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + fd, + a2, + uintptr(unsafe.Pointer(ws)), + ) + if errno != 0 { + return syscall.Errno(errno) + } + return nil +} diff --git a/vendor/github.com/creack/pty/util_solaris.go b/vendor/github.com/creack/pty/util_solaris.go new file mode 100644 index 00000000..e8896924 --- /dev/null +++ b/vendor/github.com/creack/pty/util_solaris.go @@ -0,0 +1,51 @@ +// + +package pty + +import ( + "os" + "golang.org/x/sys/unix" +) + +const ( + TIOCGWINSZ = 21608 // 'T' << 8 | 104 + TIOCSWINSZ = 21607 // 'T' << 8 | 103 +) + +// Winsize describes the terminal size. +type Winsize struct { + Rows uint16 // ws_row: Number of rows (in cells) + Cols uint16 // ws_col: Number of columns (in cells) + X uint16 // ws_xpixel: Width in pixels + Y uint16 // ws_ypixel: Height in pixels +} + +// GetsizeFull returns the full terminal size description. +func GetsizeFull(t *os.File) (size *Winsize, err error) { + var wsz *unix.Winsize + wsz, err = unix.IoctlGetWinsize(int(t.Fd()), TIOCGWINSZ) + + if err != nil { + return nil, err + } else { + return &Winsize{wsz.Row, wsz.Col, wsz.Xpixel, wsz.Ypixel}, nil + } +} + +// Get Windows Size +func Getsize(t *os.File) (rows, cols int, err error) { + var wsz *unix.Winsize + wsz, err = unix.IoctlGetWinsize(int(t.Fd()), TIOCGWINSZ) + + if err != nil { + return 80, 25, err + } else { + return int(wsz.Row), int(wsz.Col), nil + } +} + +// Setsize resizes t to s. +func Setsize(t *os.File, ws *Winsize) error { + wsz := unix.Winsize{ws.Rows, ws.Cols, ws.X, ws.Y} + return unix.IoctlSetWinsize(int(t.Fd()), TIOCSWINSZ, &wsz) +} diff --git a/vendor/github.com/creack/pty/ztypes_386.go b/vendor/github.com/creack/pty/ztypes_386.go new file mode 100644 index 00000000..ff0b8fd8 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_386.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/creack/pty/ztypes_amd64.go b/vendor/github.com/creack/pty/ztypes_amd64.go new file mode 100644 index 00000000..ff0b8fd8 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_amd64.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/creack/pty/ztypes_arm.go b/vendor/github.com/creack/pty/ztypes_arm.go new file mode 100644 index 00000000..ff0b8fd8 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_arm.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/creack/pty/ztypes_arm64.go b/vendor/github.com/creack/pty/ztypes_arm64.go new file mode 100644 index 00000000..6c29a4b9 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_arm64.go @@ -0,0 +1,11 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +// +build arm64 + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/creack/pty/ztypes_dragonfly_amd64.go b/vendor/github.com/creack/pty/ztypes_dragonfly_amd64.go new file mode 100644 index 00000000..6b0ba037 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_dragonfly_amd64.go @@ -0,0 +1,14 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_dragonfly.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Name *byte + Len uint32 + Pad_cgo_0 [4]byte +} diff --git a/vendor/github.com/creack/pty/ztypes_freebsd_386.go b/vendor/github.com/creack/pty/ztypes_freebsd_386.go new file mode 100644 index 00000000..d9975374 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_freebsd_386.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Buf *byte +} diff --git a/vendor/github.com/creack/pty/ztypes_freebsd_amd64.go b/vendor/github.com/creack/pty/ztypes_freebsd_amd64.go new file mode 100644 index 00000000..5fa102fc --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_freebsd_amd64.go @@ -0,0 +1,14 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Pad_cgo_0 [4]byte + Buf *byte +} diff --git a/vendor/github.com/creack/pty/ztypes_freebsd_arm.go b/vendor/github.com/creack/pty/ztypes_freebsd_arm.go new file mode 100644 index 00000000..d9975374 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_freebsd_arm.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Buf *byte +} diff --git a/vendor/github.com/creack/pty/ztypes_mipsx.go b/vendor/github.com/creack/pty/ztypes_mipsx.go new file mode 100644 index 00000000..f0ce7408 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_mipsx.go @@ -0,0 +1,12 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +// +build linux +// +build mips mipsle mips64 mips64le + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/creack/pty/ztypes_openbsd_386.go b/vendor/github.com/creack/pty/ztypes_openbsd_386.go new file mode 100644 index 00000000..ccb3aab9 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_openbsd_386.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_openbsd.go + +package pty + +type ptmget struct { + Cfd int32 + Sfd int32 + Cn [16]int8 + Sn [16]int8 +} + +var ioctl_PTMGET = 0x40287401 diff --git a/vendor/github.com/creack/pty/ztypes_openbsd_amd64.go b/vendor/github.com/creack/pty/ztypes_openbsd_amd64.go new file mode 100644 index 00000000..e6705168 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_openbsd_amd64.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_openbsd.go + +package pty + +type ptmget struct { + Cfd int32 + Sfd int32 + Cn [16]int8 + Sn [16]int8 +} + +var ioctl_PTMGET = 0x40287401 diff --git a/vendor/github.com/creack/pty/ztypes_ppc64.go b/vendor/github.com/creack/pty/ztypes_ppc64.go new file mode 100644 index 00000000..4e1af843 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_ppc64.go @@ -0,0 +1,11 @@ +// +build ppc64 + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/creack/pty/ztypes_ppc64le.go b/vendor/github.com/creack/pty/ztypes_ppc64le.go new file mode 100644 index 00000000..e6780f4e --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_ppc64le.go @@ -0,0 +1,11 @@ +// +build ppc64le + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/creack/pty/ztypes_riscvx.go b/vendor/github.com/creack/pty/ztypes_riscvx.go new file mode 100644 index 00000000..99eec8ec --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_riscvx.go @@ -0,0 +1,11 @@ +// Code generated by cmd/cgo -godefs; DO NOT EDIT. +// cgo -godefs types.go + +// +build riscv riscv64 + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/creack/pty/ztypes_s390x.go b/vendor/github.com/creack/pty/ztypes_s390x.go new file mode 100644 index 00000000..a7452b61 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_s390x.go @@ -0,0 +1,11 @@ +// +build s390x + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/gliderlabs/ssh/LICENSE b/vendor/github.com/gliderlabs/ssh/LICENSE new file mode 100644 index 00000000..4a03f02a --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2016 Glider Labs. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Glider Labs nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gliderlabs/ssh/README.md b/vendor/github.com/gliderlabs/ssh/README.md new file mode 100644 index 00000000..0e976e34 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/README.md @@ -0,0 +1,96 @@ +# gliderlabs/ssh + +[![GoDoc](https://godoc.org/github.com/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh) +[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh) +[![Go Report Card](https://goreportcard.com/badge/github.com/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh) +[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors) +[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com) +[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312) + +> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member + +This Go package wraps the [crypto/ssh +package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for +building SSH servers. The goal of the API was to make it as simple as using +[net/http](https://golang.org/pkg/net/http/), so the API is very similar: + +```go + package main + + import ( + "github.com/gliderlabs/ssh" + "io" + "log" + ) + + func main() { + ssh.Handle(func(s ssh.Session) { + io.WriteString(s, "Hello world\n") + }) + + log.Fatal(ssh.ListenAndServe(":2222", nil)) + } + +``` +This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)). + +## Examples + +A bunch of great examples are in the `_examples` directory. + +## Usage + +[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh) + +## Contributing + +Pull requests are welcome! However, since this project is very much about API +design, please submit API changes as issues to discuss before submitting PRs. + +Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well. + +## Roadmap + +* Non-session channel handlers +* Cleanup callback API +* 1.0 release +* High-level client? + +## Sponsors + +Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## License + +BSD diff --git a/vendor/github.com/gliderlabs/ssh/agent.go b/vendor/github.com/gliderlabs/ssh/agent.go new file mode 100644 index 00000000..d8dcb9a0 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/agent.go @@ -0,0 +1,83 @@ +package ssh + +import ( + "io" + "io/ioutil" + "net" + "path" + "sync" + + gossh "golang.org/x/crypto/ssh" +) + +const ( + agentRequestType = "auth-agent-req@openssh.com" + agentChannelType = "auth-agent@openssh.com" + + agentTempDir = "auth-agent" + agentListenFile = "listener.sock" +) + +// contextKeyAgentRequest is an internal context key for storing if the +// client requested agent forwarding +var contextKeyAgentRequest = &contextKey{"auth-agent-req"} + +// SetAgentRequested sets up the session context so that AgentRequested +// returns true. +func SetAgentRequested(ctx Context) { + ctx.SetValue(contextKeyAgentRequest, true) +} + +// AgentRequested returns true if the client requested agent forwarding. +func AgentRequested(sess Session) bool { + return sess.Context().Value(contextKeyAgentRequest) == true +} + +// NewAgentListener sets up a temporary Unix socket that can be communicated +// to the session environment and used for forwarding connections. +func NewAgentListener() (net.Listener, error) { + dir, err := ioutil.TempDir("", agentTempDir) + if err != nil { + return nil, err + } + l, err := net.Listen("unix", path.Join(dir, agentListenFile)) + if err != nil { + return nil, err + } + return l, nil +} + +// ForwardAgentConnections takes connections from a listener to proxy into the +// session on the OpenSSH channel for agent connections. It blocks and services +// connections until the listener stop accepting. +func ForwardAgentConnections(l net.Listener, s Session) { + sshConn := s.Context().Value(ContextKeyConn).(gossh.Conn) + for { + conn, err := l.Accept() + if err != nil { + return + } + go func(conn net.Conn) { + defer conn.Close() + channel, reqs, err := sshConn.OpenChannel(agentChannelType, nil) + if err != nil { + return + } + defer channel.Close() + go gossh.DiscardRequests(reqs) + var wg sync.WaitGroup + wg.Add(2) + go func() { + io.Copy(conn, channel) + conn.(*net.UnixConn).CloseWrite() + wg.Done() + }() + go func() { + io.Copy(channel, conn) + channel.CloseWrite() + wg.Done() + }() + wg.Wait() + }(conn) + } +} diff --git a/vendor/github.com/gliderlabs/ssh/circle.yml b/vendor/github.com/gliderlabs/ssh/circle.yml new file mode 100644 index 00000000..08616b01 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/circle.yml @@ -0,0 +1,26 @@ +version: 2 +jobs: + build-go-latest: + docker: + - image: golang:latest + working_directory: /go/src/github.com/gliderlabs/ssh + steps: + - checkout + - run: go get + - run: go test -v -race + + build-go-1.9: + docker: + - image: golang:1.9 + working_directory: /go/src/github.com/gliderlabs/ssh + steps: + - checkout + - run: go get + - run: go test -v -race + +workflows: + version: 2 + build: + jobs: + - build-go-latest + - build-go-1.9 diff --git a/vendor/github.com/gliderlabs/ssh/conn.go b/vendor/github.com/gliderlabs/ssh/conn.go new file mode 100644 index 00000000..ebef8845 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/conn.go @@ -0,0 +1,55 @@ +package ssh + +import ( + "context" + "net" + "time" +) + +type serverConn struct { + net.Conn + + idleTimeout time.Duration + maxDeadline time.Time + closeCanceler context.CancelFunc +} + +func (c *serverConn) Write(p []byte) (n int, err error) { + c.updateDeadline() + n, err = c.Conn.Write(p) + if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil { + c.closeCanceler() + } + return +} + +func (c *serverConn) Read(b []byte) (n int, err error) { + c.updateDeadline() + n, err = c.Conn.Read(b) + if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil { + c.closeCanceler() + } + return +} + +func (c *serverConn) Close() (err error) { + err = c.Conn.Close() + if c.closeCanceler != nil { + c.closeCanceler() + } + return +} + +func (c *serverConn) updateDeadline() { + switch { + case c.idleTimeout > 0: + idleDeadline := time.Now().Add(c.idleTimeout) + if idleDeadline.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() { + c.Conn.SetDeadline(idleDeadline) + return + } + fallthrough + default: + c.Conn.SetDeadline(c.maxDeadline) + } +} diff --git a/vendor/github.com/gliderlabs/ssh/context.go b/vendor/github.com/gliderlabs/ssh/context.go new file mode 100644 index 00000000..2f61a40d --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/context.go @@ -0,0 +1,152 @@ +package ssh + +import ( + "context" + "encoding/hex" + "net" + "sync" + + gossh "golang.org/x/crypto/ssh" +) + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. +type contextKey struct { + name string +} + +var ( + // ContextKeyUser is a context key for use with Contexts in this package. + // The associated value will be of type string. + ContextKeyUser = &contextKey{"user"} + + // ContextKeySessionID is a context key for use with Contexts in this package. + // The associated value will be of type string. + ContextKeySessionID = &contextKey{"session-id"} + + // ContextKeyPermissions is a context key for use with Contexts in this package. + // The associated value will be of type *Permissions. + ContextKeyPermissions = &contextKey{"permissions"} + + // ContextKeyClientVersion is a context key for use with Contexts in this package. + // The associated value will be of type string. + ContextKeyClientVersion = &contextKey{"client-version"} + + // ContextKeyServerVersion is a context key for use with Contexts in this package. + // The associated value will be of type string. + ContextKeyServerVersion = &contextKey{"server-version"} + + // ContextKeyLocalAddr is a context key for use with Contexts in this package. + // The associated value will be of type net.Addr. + ContextKeyLocalAddr = &contextKey{"local-addr"} + + // ContextKeyRemoteAddr is a context key for use with Contexts in this package. + // The associated value will be of type net.Addr. + ContextKeyRemoteAddr = &contextKey{"remote-addr"} + + // ContextKeyServer is a context key for use with Contexts in this package. + // The associated value will be of type *Server. + ContextKeyServer = &contextKey{"ssh-server"} + + // ContextKeyConn is a context key for use with Contexts in this package. + // The associated value will be of type gossh.ServerConn. + ContextKeyConn = &contextKey{"ssh-conn"} + + // ContextKeyPublicKey is a context key for use with Contexts in this package. + // The associated value will be of type PublicKey. + ContextKeyPublicKey = &contextKey{"public-key"} +) + +// Context is a package specific context interface. It exposes connection +// metadata and allows new values to be easily written to it. It's used in +// authentication handlers and callbacks, and its underlying context.Context is +// exposed on Session in the session Handler. A connection-scoped lock is also +// embedded in the context to make it easier to limit operations per-connection. +type Context interface { + context.Context + sync.Locker + + // User returns the username used when establishing the SSH connection. + User() string + + // SessionID returns the session hash. + SessionID() string + + // ClientVersion returns the version reported by the client. + ClientVersion() string + + // ServerVersion returns the version reported by the server. + ServerVersion() string + + // RemoteAddr returns the remote address for this connection. + RemoteAddr() net.Addr + + // LocalAddr returns the local address for this connection. + LocalAddr() net.Addr + + // Permissions returns the Permissions object used for this connection. + Permissions() *Permissions + + // SetValue allows you to easily write new values into the underlying context. + SetValue(key, value interface{}) +} + +type sshContext struct { + context.Context + *sync.Mutex +} + +func newContext(srv *Server) (*sshContext, context.CancelFunc) { + innerCtx, cancel := context.WithCancel(context.Background()) + ctx := &sshContext{innerCtx, &sync.Mutex{}} + ctx.SetValue(ContextKeyServer, srv) + perms := &Permissions{&gossh.Permissions{}} + ctx.SetValue(ContextKeyPermissions, perms) + return ctx, cancel +} + +// this is separate from newContext because we will get ConnMetadata +// at different points so it needs to be applied separately +func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) { + if ctx.Value(ContextKeySessionID) != nil { + return + } + ctx.SetValue(ContextKeySessionID, hex.EncodeToString(conn.SessionID())) + ctx.SetValue(ContextKeyClientVersion, string(conn.ClientVersion())) + ctx.SetValue(ContextKeyServerVersion, string(conn.ServerVersion())) + ctx.SetValue(ContextKeyUser, conn.User()) + ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr()) + ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr()) +} + +func (ctx *sshContext) SetValue(key, value interface{}) { + ctx.Context = context.WithValue(ctx.Context, key, value) +} + +func (ctx *sshContext) User() string { + return ctx.Value(ContextKeyUser).(string) +} + +func (ctx *sshContext) SessionID() string { + return ctx.Value(ContextKeySessionID).(string) +} + +func (ctx *sshContext) ClientVersion() string { + return ctx.Value(ContextKeyClientVersion).(string) +} + +func (ctx *sshContext) ServerVersion() string { + return ctx.Value(ContextKeyServerVersion).(string) +} + +func (ctx *sshContext) RemoteAddr() net.Addr { + return ctx.Value(ContextKeyRemoteAddr).(net.Addr) +} + +func (ctx *sshContext) LocalAddr() net.Addr { + return ctx.Value(ContextKeyLocalAddr).(net.Addr) +} + +func (ctx *sshContext) Permissions() *Permissions { + return ctx.Value(ContextKeyPermissions).(*Permissions) +} diff --git a/vendor/github.com/gliderlabs/ssh/doc.go b/vendor/github.com/gliderlabs/ssh/doc.go new file mode 100644 index 00000000..5a10393c --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/doc.go @@ -0,0 +1,45 @@ +/* +Package ssh wraps the crypto/ssh package with a higher-level API for building +SSH servers. The goal of the API was to make it as simple as using net/http, so +the API is very similar. + +You should be able to build any SSH server using only this package, which wraps +relevant types and some functions from crypto/ssh. However, you still need to +use crypto/ssh for building SSH clients. + +ListenAndServe starts an SSH server with a given address, handler, and options. The +handler is usually nil, which means to use DefaultHandler. Handle sets DefaultHandler: + + ssh.Handle(func(s ssh.Session) { + io.WriteString(s, "Hello world\n") + }) + + log.Fatal(ssh.ListenAndServe(":2222", nil)) + +If you don't specify a host key, it will generate one every time. This is convenient +except you'll have to deal with clients being confused that the host key is different. +It's a better idea to generate or point to an existing key on your system: + + log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa"))) + +Although all options have functional option helpers, another way to control the +server's behavior is by creating a custom Server: + + s := &ssh.Server{ + Addr: ":2222", + Handler: sessionHandler, + PublicKeyHandler: authHandler, + } + s.AddHostKey(hostKeySigner) + + log.Fatal(s.ListenAndServe()) + +This package automatically handles basic SSH requests like setting environment +variables, requesting PTY, and changing window size. These requests are +processed, responded to, and any relevant state is updated. This state is then +exposed to you via the Session interface. + +The one big feature missing from the Session abstraction is signals. This was +started, but not completed. Pull Requests welcome! +*/ +package ssh diff --git a/vendor/github.com/gliderlabs/ssh/options.go b/vendor/github.com/gliderlabs/ssh/options.go new file mode 100644 index 00000000..af748985 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/options.go @@ -0,0 +1,77 @@ +package ssh + +import ( + "io/ioutil" + + gossh "golang.org/x/crypto/ssh" +) + +// PasswordAuth returns a functional option that sets PasswordHandler on the server. +func PasswordAuth(fn PasswordHandler) Option { + return func(srv *Server) error { + srv.PasswordHandler = fn + return nil + } +} + +// PublicKeyAuth returns a functional option that sets PublicKeyHandler on the server. +func PublicKeyAuth(fn PublicKeyHandler) Option { + return func(srv *Server) error { + srv.PublicKeyHandler = fn + return nil + } +} + +// HostKeyFile returns a functional option that adds HostSigners to the server +// from a PEM file at filepath. +func HostKeyFile(filepath string) Option { + return func(srv *Server) error { + pemBytes, err := ioutil.ReadFile(filepath) + if err != nil { + return err + } + + signer, err := gossh.ParsePrivateKey(pemBytes) + if err != nil { + return err + } + + srv.AddHostKey(signer) + + return nil + } +} + +// HostKeyPEM returns a functional option that adds HostSigners to the server +// from a PEM file as bytes. +func HostKeyPEM(bytes []byte) Option { + return func(srv *Server) error { + signer, err := gossh.ParsePrivateKey(bytes) + if err != nil { + return err + } + + srv.AddHostKey(signer) + + return nil + } +} + +// NoPty returns a functional option that sets PtyCallback to return false, +// denying PTY requests. +func NoPty() Option { + return func(srv *Server) error { + srv.PtyCallback = func(ctx Context, pty Pty) bool { + return false + } + return nil + } +} + +// WrapConn returns a functional option that sets ConnCallback on the server. +func WrapConn(fn ConnCallback) Option { + return func(srv *Server) error { + srv.ConnCallback = fn + return nil + } +} diff --git a/vendor/github.com/gliderlabs/ssh/server.go b/vendor/github.com/gliderlabs/ssh/server.go new file mode 100644 index 00000000..9ff09169 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/server.go @@ -0,0 +1,394 @@ +package ssh + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "time" + + gossh "golang.org/x/crypto/ssh" +) + +// ErrServerClosed is returned by the Server's Serve, ListenAndServe, +// and ListenAndServeTLS methods after a call to Shutdown or Close. +var ErrServerClosed = errors.New("ssh: Server closed") + +type RequestHandler func(ctx Context, srv *Server, req *gossh.Request) (ok bool, payload []byte) + +var DefaultRequestHandlers = map[string]RequestHandler{} + +type ChannelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) + +var DefaultChannelHandlers = map[string]ChannelHandler{ + "session": DefaultSessionHandler, +} + +// Server defines parameters for running an SSH server. The zero value for +// Server is a valid configuration. When both PasswordHandler and +// PublicKeyHandler are nil, no client authentication is performed. +type Server struct { + Addr string // TCP address to listen on, ":22" if empty + Handler Handler // handler to invoke, ssh.DefaultHandler if nil + HostSigners []Signer // private keys for the host key, must have at least one + Version string // server version to be sent before the initial handshake + + KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler + PasswordHandler PasswordHandler // password authentication handler + PublicKeyHandler PublicKeyHandler // public key authentication handler + PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil + ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling + LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil + ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil + ServerConfigCallback ServerConfigCallback // callback for configuring detailed SSH options + SessionRequestCallback SessionRequestCallback // callback for allowing or denying SSH sessions + + IdleTimeout time.Duration // connection timeout when no activity, none if empty + MaxTimeout time.Duration // absolute connection timeout, none if empty + + // ChannelHandlers allow overriding the built-in session handlers or provide + // extensions to the protocol, such as tcpip forwarding. By default only the + // "session" handler is enabled. + ChannelHandlers map[string]ChannelHandler + + // RequestHandlers allow overriding the server-level request handlers or + // provide extensions to the protocol, such as tcpip forwarding. By default + // no handlers are enabled. + RequestHandlers map[string]RequestHandler + + listenerWg sync.WaitGroup + mu sync.Mutex + listeners map[net.Listener]struct{} + conns map[*gossh.ServerConn]struct{} + connWg sync.WaitGroup + doneChan chan struct{} +} + +func (srv *Server) ensureHostSigner() error { + if len(srv.HostSigners) == 0 { + signer, err := generateSigner() + if err != nil { + return err + } + srv.HostSigners = append(srv.HostSigners, signer) + } + return nil +} + +func (srv *Server) ensureHandlers() { + srv.mu.Lock() + defer srv.mu.Unlock() + if srv.RequestHandlers == nil { + srv.RequestHandlers = map[string]RequestHandler{} + for k, v := range DefaultRequestHandlers { + srv.RequestHandlers[k] = v + } + } + if srv.ChannelHandlers == nil { + srv.ChannelHandlers = map[string]ChannelHandler{} + for k, v := range DefaultChannelHandlers { + srv.ChannelHandlers[k] = v + } + } +} + +func (srv *Server) config(ctx Context) *gossh.ServerConfig { + var config *gossh.ServerConfig + if srv.ServerConfigCallback == nil { + config = &gossh.ServerConfig{} + } else { + config = srv.ServerConfigCallback(ctx) + } + for _, signer := range srv.HostSigners { + config.AddHostKey(signer) + } + if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil { + config.NoClientAuth = true + } + if srv.Version != "" { + config.ServerVersion = "SSH-2.0-" + srv.Version + } + if srv.PasswordHandler != nil { + config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) { + applyConnMetadata(ctx, conn) + if ok := srv.PasswordHandler(ctx, string(password)); !ok { + return ctx.Permissions().Permissions, fmt.Errorf("permission denied") + } + return ctx.Permissions().Permissions, nil + } + } + if srv.PublicKeyHandler != nil { + config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) { + applyConnMetadata(ctx, conn) + if ok := srv.PublicKeyHandler(ctx, key); !ok { + return ctx.Permissions().Permissions, fmt.Errorf("permission denied") + } + ctx.SetValue(ContextKeyPublicKey, key) + return ctx.Permissions().Permissions, nil + } + } + if srv.KeyboardInteractiveHandler != nil { + config.KeyboardInteractiveCallback = func(conn gossh.ConnMetadata, challenger gossh.KeyboardInteractiveChallenge) (*gossh.Permissions, error) { + if ok := srv.KeyboardInteractiveHandler(ctx, challenger); !ok { + return ctx.Permissions().Permissions, fmt.Errorf("permission denied") + } + return ctx.Permissions().Permissions, nil + } + } + return config +} + +// Handle sets the Handler for the server. +func (srv *Server) Handle(fn Handler) { + srv.Handler = fn +} + +// Close immediately closes all active listeners and all active +// connections. +// +// Close returns any error returned from closing the Server's +// underlying Listener(s). +func (srv *Server) Close() error { + srv.mu.Lock() + defer srv.mu.Unlock() + srv.closeDoneChanLocked() + err := srv.closeListenersLocked() + for c := range srv.conns { + c.Close() + delete(srv.conns, c) + } + return err +} + +// Shutdown gracefully shuts down the server without interrupting any +// active connections. Shutdown works by first closing all open +// listeners, and then waiting indefinitely for connections to close. +// If the provided context expires before the shutdown is complete, +// then the context's error is returned. +func (srv *Server) Shutdown(ctx context.Context) error { + srv.mu.Lock() + lnerr := srv.closeListenersLocked() + srv.closeDoneChanLocked() + srv.mu.Unlock() + + finished := make(chan struct{}, 1) + go func() { + srv.listenerWg.Wait() + srv.connWg.Wait() + finished <- struct{}{} + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case <-finished: + return lnerr + } +} + +// Serve accepts incoming connections on the Listener l, creating a new +// connection goroutine for each. The connection goroutines read requests and then +// calls srv.Handler to handle sessions. +// +// Serve always returns a non-nil error. +func (srv *Server) Serve(l net.Listener) error { + srv.ensureHandlers() + defer l.Close() + if err := srv.ensureHostSigner(); err != nil { + return err + } + if srv.Handler == nil { + srv.Handler = DefaultHandler + } + var tempDelay time.Duration + + srv.trackListener(l, true) + defer srv.trackListener(l, false) + for { + conn, e := l.Accept() + if e != nil { + select { + case <-srv.getDoneChan(): + return ErrServerClosed + default: + } + 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 + } + time.Sleep(tempDelay) + continue + } + return e + } + go srv.handleConn(conn) + } +} + +func (srv *Server) handleConn(newConn net.Conn) { + if srv.ConnCallback != nil { + cbConn := srv.ConnCallback(newConn) + if cbConn == nil { + newConn.Close() + return + } + newConn = cbConn + } + ctx, cancel := newContext(srv) + conn := &serverConn{ + Conn: newConn, + idleTimeout: srv.IdleTimeout, + closeCanceler: cancel, + } + if srv.MaxTimeout > 0 { + conn.maxDeadline = time.Now().Add(srv.MaxTimeout) + } + defer conn.Close() + sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx)) + if err != nil { + // TODO: trigger event callback + return + } + + srv.trackConn(sshConn, true) + defer srv.trackConn(sshConn, false) + + ctx.SetValue(ContextKeyConn, sshConn) + applyConnMetadata(ctx, sshConn) + //go gossh.DiscardRequests(reqs) + go srv.handleRequests(ctx, reqs) + for ch := range chans { + handler := srv.ChannelHandlers[ch.ChannelType()] + if handler == nil { + handler = srv.ChannelHandlers["default"] + } + if handler == nil { + ch.Reject(gossh.UnknownChannelType, "unsupported channel type") + continue + } + go handler(srv, sshConn, ch, ctx) + } +} + +func (srv *Server) handleRequests(ctx Context, in <-chan *gossh.Request) { + for req := range in { + handler := srv.RequestHandlers[req.Type] + if handler == nil { + handler = srv.RequestHandlers["default"] + } + if handler == nil { + req.Reply(false, nil) + continue + } + /*reqCtx, cancel := context.WithCancel(ctx) + defer cancel() */ + ret, payload := handler(ctx, srv, req) + req.Reply(ret, payload) + } +} + +// ListenAndServe listens on the TCP network address srv.Addr and then calls +// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used. +// ListenAndServe always returns a non-nil error. +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) +} + +// AddHostKey adds a private key as a host key. If an existing host key exists +// with the same algorithm, it is overwritten. Each server config must have at +// least one host key. +func (srv *Server) AddHostKey(key Signer) { + // these are later added via AddHostKey on ServerConfig, which performs the + // check for one of every algorithm. + srv.HostSigners = append(srv.HostSigners, key) +} + +// SetOption runs a functional option against the server. +func (srv *Server) SetOption(option Option) error { + return option(srv) +} + +func (srv *Server) getDoneChan() <-chan struct{} { + srv.mu.Lock() + defer srv.mu.Unlock() + return srv.getDoneChanLocked() +} + +func (srv *Server) getDoneChanLocked() chan struct{} { + if srv.doneChan == nil { + srv.doneChan = make(chan struct{}) + } + return srv.doneChan +} + +func (srv *Server) closeDoneChanLocked() { + ch := srv.getDoneChanLocked() + select { + case <-ch: + // Already closed. Don't close again. + default: + // Safe to close here. We're the only closer, guarded + // by srv.mu. + close(ch) + } +} + +func (srv *Server) closeListenersLocked() error { + var err error + for ln := range srv.listeners { + if cerr := ln.Close(); cerr != nil && err == nil { + err = cerr + } + delete(srv.listeners, ln) + } + return err +} + +func (srv *Server) trackListener(ln net.Listener, add bool) { + srv.mu.Lock() + defer srv.mu.Unlock() + if srv.listeners == nil { + srv.listeners = make(map[net.Listener]struct{}) + } + if add { + // If the *Server is being reused after a previous + // Close or Shutdown, reset its doneChan: + if len(srv.listeners) == 0 && len(srv.conns) == 0 { + srv.doneChan = nil + } + srv.listeners[ln] = struct{}{} + srv.listenerWg.Add(1) + } else { + delete(srv.listeners, ln) + srv.listenerWg.Done() + } +} + +func (srv *Server) trackConn(c *gossh.ServerConn, add bool) { + srv.mu.Lock() + defer srv.mu.Unlock() + if srv.conns == nil { + srv.conns = make(map[*gossh.ServerConn]struct{}) + } + if add { + srv.conns[c] = struct{}{} + srv.connWg.Add(1) + } else { + delete(srv.conns, c) + srv.connWg.Done() + } +} diff --git a/vendor/github.com/gliderlabs/ssh/session.go b/vendor/github.com/gliderlabs/ssh/session.go new file mode 100644 index 00000000..6c77d6c8 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/session.go @@ -0,0 +1,308 @@ +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) + } + } +} diff --git a/vendor/github.com/gliderlabs/ssh/ssh.go b/vendor/github.com/gliderlabs/ssh/ssh.go new file mode 100644 index 00000000..f5a935a9 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/ssh.go @@ -0,0 +1,123 @@ +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" +) + +// DefaultHandler is the default Handler used by Serve. +var DefaultHandler Handler + +// Option is a functional option handler for Server. +type Option func(*Server) error + +// Handler is a callback for handling established SSH sessions. +type Handler func(Session) + +// PublicKeyHandler is a callback for performing public key authentication. +type PublicKeyHandler func(ctx Context, key PublicKey) bool + +// PasswordHandler is a callback for performing password authentication. +type PasswordHandler func(ctx Context, password string) bool + +// KeyboardInteractiveHandler is a callback for performing keyboard-interactive authentication. +type KeyboardInteractiveHandler func(ctx Context, challenger gossh.KeyboardInteractiveChallenge) bool + +// PtyCallback is a hook for allowing PTY sessions. +type PtyCallback func(ctx Context, pty Pty) bool + +// SessionRequestCallback is a callback for allowing or denying SSH sessions. +type SessionRequestCallback func(sess Session, requestType string) bool + +// ConnCallback is a hook for new connections before handling. +// It allows wrapping for timeouts and limiting by returning +// the net.Conn that will be used as the underlying connection. +type ConnCallback func(conn net.Conn) net.Conn + +// LocalPortForwardingCallback is a hook for allowing port forwarding +type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool + +// ReversePortForwardingCallback is a hook for allowing reverse port forwarding +type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool + +// ServerConfigCallback is a hook for creating custom default server configs +type ServerConfigCallback func(ctx Context) *gossh.ServerConfig + +// Window represents the size of a PTY window. +type Window struct { + Width int + Height int +} + +// Pty represents a PTY request and configuration. +type Pty struct { + Term string + Window Window + // HELP WANTED: terminal modes! +} + +// Serve accepts incoming SSH connections on the listener l, creating a new +// connection goroutine for each. The connection goroutines read requests and +// then calls handler to handle sessions. Handler is typically nil, in which +// case the DefaultHandler is used. +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) +} + +// ListenAndServe listens on the TCP network address addr and then calls Serve +// with handler to handle sessions on incoming connections. Handler is typically +// nil, in which case the DefaultHandler is used. +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() +} + +// Handle registers the handler as the DefaultHandler. +func Handle(handler Handler) { + DefaultHandler = handler +} + +// KeysEqual is constant time compare of the keys to avoid timing attacks. +func KeysEqual(ak, bk PublicKey) bool { + + //avoid panic if one of the keys is nil, return false instead + if ak == nil || bk == nil { + return false + } + + a := ak.Marshal() + b := bk.Marshal() + return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1) +} diff --git a/vendor/github.com/gliderlabs/ssh/tcpip.go b/vendor/github.com/gliderlabs/ssh/tcpip.go new file mode 100644 index 00000000..335fda65 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/tcpip.go @@ -0,0 +1,193 @@ +package ssh + +import ( + "io" + "log" + "net" + "strconv" + "sync" + + gossh "golang.org/x/crypto/ssh" +) + +const ( + forwardedTCPChannelType = "forwarded-tcpip" +) + +// direct-tcpip data struct as specified in RFC4254, Section 7.2 +type localForwardChannelData struct { + DestAddr string + DestPort uint32 + + OriginAddr string + OriginPort uint32 +} + +// DirectTCPIPHandler can be enabled by adding it to the server's +// ChannelHandlers under direct-tcpip. +func DirectTCPIPHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) { + d := localForwardChannelData{} + if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil { + newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error()) + return + } + + if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestAddr, d.DestPort) { + newChan.Reject(gossh.Prohibited, "port forwarding is disabled") + return + } + + dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10)) + + var dialer net.Dialer + dconn, err := dialer.DialContext(ctx, "tcp", dest) + if err != nil { + newChan.Reject(gossh.ConnectionFailed, err.Error()) + return + } + + ch, reqs, err := newChan.Accept() + if err != nil { + dconn.Close() + return + } + go gossh.DiscardRequests(reqs) + + go func() { + defer ch.Close() + defer dconn.Close() + io.Copy(ch, dconn) + }() + go func() { + defer ch.Close() + defer dconn.Close() + io.Copy(dconn, ch) + }() +} + +type remoteForwardRequest struct { + BindAddr string + BindPort uint32 +} + +type remoteForwardSuccess struct { + BindPort uint32 +} + +type remoteForwardCancelRequest struct { + BindAddr string + BindPort uint32 +} + +type remoteForwardChannelData struct { + DestAddr string + DestPort uint32 + OriginAddr string + OriginPort uint32 +} + +// ForwardedTCPHandler can be enabled by creating a ForwardedTCPHandler and +// adding the HandleSSHRequest callback to the server's RequestHandlers under +// tcpip-forward and cancel-tcpip-forward. +type ForwardedTCPHandler struct { + forwards map[string]net.Listener + sync.Mutex +} + +func (h *ForwardedTCPHandler) HandleSSHRequest(ctx Context, srv *Server, req *gossh.Request) (bool, []byte) { + h.Lock() + if h.forwards == nil { + h.forwards = make(map[string]net.Listener) + } + h.Unlock() + conn := ctx.Value(ContextKeyConn).(*gossh.ServerConn) + switch req.Type { + case "tcpip-forward": + var reqPayload remoteForwardRequest + if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil { + // TODO: log parse failure + return false, []byte{} + } + if srv.ReversePortForwardingCallback == nil || !srv.ReversePortForwardingCallback(ctx, reqPayload.BindAddr, reqPayload.BindPort) { + return false, []byte("port forwarding is disabled") + } + addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort))) + ln, err := net.Listen("tcp", addr) + if err != nil { + // TODO: log listen failure + return false, []byte{} + } + _, destPortStr, _ := net.SplitHostPort(ln.Addr().String()) + destPort, _ := strconv.Atoi(destPortStr) + h.Lock() + h.forwards[addr] = ln + h.Unlock() + go func() { + <-ctx.Done() + h.Lock() + ln, ok := h.forwards[addr] + h.Unlock() + if ok { + ln.Close() + } + }() + go func() { + for { + c, err := ln.Accept() + if err != nil { + // TODO: log accept failure + break + } + originAddr, orignPortStr, _ := net.SplitHostPort(c.RemoteAddr().String()) + originPort, _ := strconv.Atoi(orignPortStr) + payload := gossh.Marshal(&remoteForwardChannelData{ + DestAddr: reqPayload.BindAddr, + DestPort: uint32(destPort), + OriginAddr: originAddr, + OriginPort: uint32(originPort), + }) + go func() { + ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload) + if err != nil { + // TODO: log failure to open channel + log.Println(err) + c.Close() + return + } + go gossh.DiscardRequests(reqs) + go func() { + defer ch.Close() + defer c.Close() + io.Copy(ch, c) + }() + go func() { + defer ch.Close() + defer c.Close() + io.Copy(c, ch) + }() + }() + } + h.Lock() + delete(h.forwards, addr) + h.Unlock() + }() + return true, gossh.Marshal(&remoteForwardSuccess{uint32(destPort)}) + + case "cancel-tcpip-forward": + var reqPayload remoteForwardCancelRequest + if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil { + // TODO: log parse failure + return false, []byte{} + } + addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort))) + h.Lock() + ln, ok := h.forwards[addr] + h.Unlock() + if ok { + ln.Close() + } + return true, nil + default: + return false, nil + } +} diff --git a/vendor/github.com/gliderlabs/ssh/util.go b/vendor/github.com/gliderlabs/ssh/util.go new file mode 100644 index 00000000..015a44ec --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/util.go @@ -0,0 +1,83 @@ +package ssh + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/binary" + + "golang.org/x/crypto/ssh" +) + +func generateSigner() (ssh.Signer, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + return ssh.NewSignerFromKey(key) +} + +func parsePtyRequest(s []byte) (pty Pty, ok bool) { + term, s, ok := parseString(s) + if !ok { + return + } + width32, s, ok := parseUint32(s) + if !ok { + return + } + height32, _, ok := parseUint32(s) + if !ok { + return + } + pty = Pty{ + Term: term, + Window: Window{ + Width: int(width32), + Height: int(height32), + }, + } + return +} + +func parseWinchRequest(s []byte) (win Window, ok bool) { + width32, s, ok := parseUint32(s) + if width32 < 1 { + ok = false + } + if !ok { + return + } + height32, _, ok := parseUint32(s) + if height32 < 1 { + ok = false + } + if !ok { + return + } + win = Window{ + Width: int(width32), + Height: int(height32), + } + 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/vendor/github.com/gliderlabs/ssh/wrap.go b/vendor/github.com/gliderlabs/ssh/wrap.go new file mode 100644 index 00000000..d1f2b161 --- /dev/null +++ b/vendor/github.com/gliderlabs/ssh/wrap.go @@ -0,0 +1,33 @@ +package ssh + +import gossh "golang.org/x/crypto/ssh" + +// PublicKey is an abstraction of different types of public keys. +type PublicKey interface { + gossh.PublicKey +} + +// The Permissions type holds fine-grained permissions that are specific to a +// user or a specific authentication method for a user. Permissions, except for +// "source-address", must be enforced in the server application layer, after +// successful authentication. +type Permissions struct { + *gossh.Permissions +} + +// A Signer can create signatures that verify against a public key. +type Signer interface { + gossh.Signer +} + +// ParseAuthorizedKey parses a public key from an authorized_keys file used in +// OpenSSH according to the sshd(8) manual page. +func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) { + return gossh.ParseAuthorizedKey(in) +} + +// ParsePublicKey parses an SSH public key formatted for use in +// the SSH wire protocol according to RFC 4253, section 6.6. +func ParsePublicKey(in []byte) (out PublicKey, err error) { + return gossh.ParsePublicKey(in) +}