diff options
Diffstat (limited to 'tempfork')
| -rw-r--r-- | tempfork/gliderlabs/ssh/LICENSE | 54 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/README.md | 192 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/agent.go | 166 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/conn.go | 110 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/context.go | 328 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/context_test.go | 98 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/doc.go | 90 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/example_test.go | 100 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/options.go | 168 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/options_test.go | 222 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/server.go | 918 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/server_test.go | 256 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/session.go | 772 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/session_test.go | 880 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/ssh.go | 312 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/ssh_test.go | 34 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/tcpip.go | 386 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/tcpip_test.go | 170 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/util.go | 314 | ||||
| -rw-r--r-- | tempfork/gliderlabs/ssh/wrap.go | 66 | ||||
| -rw-r--r-- | tempfork/heap/heap.go | 242 |
21 files changed, 2939 insertions, 2939 deletions
diff --git a/tempfork/gliderlabs/ssh/LICENSE b/tempfork/gliderlabs/ssh/LICENSE index 4a03f02a2..80b2b2baa 100644 --- a/tempfork/gliderlabs/ssh/LICENSE +++ b/tempfork/gliderlabs/ssh/LICENSE @@ -1,27 +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. +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/tempfork/gliderlabs/ssh/README.md b/tempfork/gliderlabs/ssh/README.md index 79b5b89fa..ecef6b7c4 100644 --- a/tempfork/gliderlabs/ssh/README.md +++ b/tempfork/gliderlabs/ssh/README.md @@ -1,96 +1,96 @@ -# gliderlabs/ssh - -[](https://godoc.org/github.com/gliderlabs/ssh) -[](https://circleci.com/gh/gliderlabs/ssh) -[](https://goreportcard.com/report/github.com/gliderlabs/ssh) -[](#sponsors) -[](http://slack.gliderlabs.com) -[](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 ( - "tailscale.com/tempfork/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/tailscale.com/tempfork/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)] - -<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a> -<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a> - -## License - -[BSD](LICENSE) +# gliderlabs/ssh
+
+[](https://godoc.org/github.com/gliderlabs/ssh)
+[](https://circleci.com/gh/gliderlabs/ssh)
+[](https://goreportcard.com/report/github.com/gliderlabs/ssh)
+[](#sponsors)
+[](http://slack.gliderlabs.com)
+[](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 (
+ "tailscale.com/tempfork/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/tailscale.com/tempfork/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)]
+
+<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a>
+<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a>
+
+## License
+
+[BSD](LICENSE)
diff --git a/tempfork/gliderlabs/ssh/agent.go b/tempfork/gliderlabs/ssh/agent.go index 86a5bce7f..3da665292 100644 --- a/tempfork/gliderlabs/ssh/agent.go +++ b/tempfork/gliderlabs/ssh/agent.go @@ -1,83 +1,83 @@ -package ssh - -import ( - "io" - "net" - "os" - "path" - "sync" - - gossh "github.com/tailscale/golang-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 := os.MkdirTemp("", 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) - } -} +package ssh
+
+import (
+ "io"
+ "net"
+ "os"
+ "path"
+ "sync"
+
+ gossh "github.com/tailscale/golang-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 := os.MkdirTemp("", 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/tempfork/gliderlabs/ssh/conn.go b/tempfork/gliderlabs/ssh/conn.go index ebef8845b..ec277bf27 100644 --- a/tempfork/gliderlabs/ssh/conn.go +++ b/tempfork/gliderlabs/ssh/conn.go @@ -1,55 +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) - } -} +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/tempfork/gliderlabs/ssh/context.go b/tempfork/gliderlabs/ssh/context.go index d43de6f09..6f7245574 100644 --- a/tempfork/gliderlabs/ssh/context.go +++ b/tempfork/gliderlabs/ssh/context.go @@ -1,164 +1,164 @@ -package ssh - -import ( - "context" - "encoding/hex" - "net" - "sync" - - gossh "github.com/tailscale/golang-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"} - - ContextKeySendAuthBanner = &contextKey{"send-auth-banner"} -) - -// 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{}) - - SendAuthBanner(banner string) error -} - -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()) - ctx.SetValue(ContextKeySendAuthBanner, conn.SendAuthBanner) -} - -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 { - if addr, ok := ctx.Value(ContextKeyRemoteAddr).(net.Addr); ok { - return addr - } - return nil -} - -func (ctx *sshContext) LocalAddr() net.Addr { - return ctx.Value(ContextKeyLocalAddr).(net.Addr) -} - -func (ctx *sshContext) Permissions() *Permissions { - return ctx.Value(ContextKeyPermissions).(*Permissions) -} - -func (ctx *sshContext) SendAuthBanner(msg string) error { - return ctx.Value(ContextKeySendAuthBanner).(func(string) error)(msg) -} +package ssh
+
+import (
+ "context"
+ "encoding/hex"
+ "net"
+ "sync"
+
+ gossh "github.com/tailscale/golang-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"}
+
+ ContextKeySendAuthBanner = &contextKey{"send-auth-banner"}
+)
+
+// 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{})
+
+ SendAuthBanner(banner string) error
+}
+
+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())
+ ctx.SetValue(ContextKeySendAuthBanner, conn.SendAuthBanner)
+}
+
+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 {
+ if addr, ok := ctx.Value(ContextKeyRemoteAddr).(net.Addr); ok {
+ return addr
+ }
+ return nil
+}
+
+func (ctx *sshContext) LocalAddr() net.Addr {
+ return ctx.Value(ContextKeyLocalAddr).(net.Addr)
+}
+
+func (ctx *sshContext) Permissions() *Permissions {
+ return ctx.Value(ContextKeyPermissions).(*Permissions)
+}
+
+func (ctx *sshContext) SendAuthBanner(msg string) error {
+ return ctx.Value(ContextKeySendAuthBanner).(func(string) error)(msg)
+}
diff --git a/tempfork/gliderlabs/ssh/context_test.go b/tempfork/gliderlabs/ssh/context_test.go index dcbd326b7..8f71c3958 100644 --- a/tempfork/gliderlabs/ssh/context_test.go +++ b/tempfork/gliderlabs/ssh/context_test.go @@ -1,49 +1,49 @@ -//go:build glidertests - -package ssh - -import "testing" - -func TestSetPermissions(t *testing.T) { - t.Parallel() - permsExt := map[string]string{ - "foo": "bar", - } - session, _, cleanup := newTestSessionWithOptions(t, &Server{ - Handler: func(s Session) { - if _, ok := s.Permissions().Extensions["foo"]; !ok { - t.Fatalf("got %#v; want %#v", s.Permissions().Extensions, permsExt) - } - }, - }, nil, PasswordAuth(func(ctx Context, password string) bool { - ctx.Permissions().Extensions = permsExt - return true - })) - defer cleanup() - if err := session.Run(""); err != nil { - t.Fatal(err) - } -} - -func TestSetValue(t *testing.T) { - t.Parallel() - value := map[string]string{ - "foo": "bar", - } - key := "testValue" - session, _, cleanup := newTestSessionWithOptions(t, &Server{ - Handler: func(s Session) { - v := s.Context().Value(key).(map[string]string) - if v["foo"] != value["foo"] { - t.Fatalf("got %#v; want %#v", v, value) - } - }, - }, nil, PasswordAuth(func(ctx Context, password string) bool { - ctx.SetValue(key, value) - return true - })) - defer cleanup() - if err := session.Run(""); err != nil { - t.Fatal(err) - } -} +//go:build glidertests
+
+package ssh
+
+import "testing"
+
+func TestSetPermissions(t *testing.T) {
+ t.Parallel()
+ permsExt := map[string]string{
+ "foo": "bar",
+ }
+ session, _, cleanup := newTestSessionWithOptions(t, &Server{
+ Handler: func(s Session) {
+ if _, ok := s.Permissions().Extensions["foo"]; !ok {
+ t.Fatalf("got %#v; want %#v", s.Permissions().Extensions, permsExt)
+ }
+ },
+ }, nil, PasswordAuth(func(ctx Context, password string) bool {
+ ctx.Permissions().Extensions = permsExt
+ return true
+ }))
+ defer cleanup()
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetValue(t *testing.T) {
+ t.Parallel()
+ value := map[string]string{
+ "foo": "bar",
+ }
+ key := "testValue"
+ session, _, cleanup := newTestSessionWithOptions(t, &Server{
+ Handler: func(s Session) {
+ v := s.Context().Value(key).(map[string]string)
+ if v["foo"] != value["foo"] {
+ t.Fatalf("got %#v; want %#v", v, value)
+ }
+ },
+ }, nil, PasswordAuth(func(ctx Context, password string) bool {
+ ctx.SetValue(key, value)
+ return true
+ }))
+ defer cleanup()
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/doc.go b/tempfork/gliderlabs/ssh/doc.go index d13919176..46c47d650 100644 --- a/tempfork/gliderlabs/ssh/doc.go +++ b/tempfork/gliderlabs/ssh/doc.go @@ -1,45 +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 +/*
+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/tempfork/gliderlabs/ssh/example_test.go b/tempfork/gliderlabs/ssh/example_test.go index c174bc4ae..61ffebbc0 100644 --- a/tempfork/gliderlabs/ssh/example_test.go +++ b/tempfork/gliderlabs/ssh/example_test.go @@ -1,50 +1,50 @@ -package ssh_test - -import ( - "errors" - "io" - "os" - - "tailscale.com/tempfork/gliderlabs/ssh" -) - -func ExampleListenAndServe() { - ssh.ListenAndServe(":2222", func(s ssh.Session) { - io.WriteString(s, "Hello world\n") - }) -} - -func ExamplePasswordAuth() { - ssh.ListenAndServe(":2222", nil, - ssh.PasswordAuth(func(ctx ssh.Context, pass string) bool { - return pass == "secret" - }), - ) -} - -func ExampleNoPty() { - ssh.ListenAndServe(":2222", nil, ssh.NoPty()) -} - -func ExamplePublicKeyAuth() { - ssh.ListenAndServe(":2222", nil, - ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) error { - data, err := os.ReadFile("/path/to/allowed/key.pub") - if err != nil { - return err - } - allowed, _, _, _, err := ssh.ParseAuthorizedKey(data) - if err != nil { - return err - } - if !ssh.KeysEqual(key, allowed) { - return errors.New("some error") - } - return nil - }), - ) -} - -func ExampleHostKeyFile() { - ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/path/to/host/key")) -} +package ssh_test
+
+import (
+ "errors"
+ "io"
+ "os"
+
+ "tailscale.com/tempfork/gliderlabs/ssh"
+)
+
+func ExampleListenAndServe() {
+ ssh.ListenAndServe(":2222", func(s ssh.Session) {
+ io.WriteString(s, "Hello world\n")
+ })
+}
+
+func ExamplePasswordAuth() {
+ ssh.ListenAndServe(":2222", nil,
+ ssh.PasswordAuth(func(ctx ssh.Context, pass string) bool {
+ return pass == "secret"
+ }),
+ )
+}
+
+func ExampleNoPty() {
+ ssh.ListenAndServe(":2222", nil, ssh.NoPty())
+}
+
+func ExamplePublicKeyAuth() {
+ ssh.ListenAndServe(":2222", nil,
+ ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) error {
+ data, err := os.ReadFile("/path/to/allowed/key.pub")
+ if err != nil {
+ return err
+ }
+ allowed, _, _, _, err := ssh.ParseAuthorizedKey(data)
+ if err != nil {
+ return err
+ }
+ if !ssh.KeysEqual(key, allowed) {
+ return errors.New("some error")
+ }
+ return nil
+ }),
+ )
+}
+
+func ExampleHostKeyFile() {
+ ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/path/to/host/key"))
+}
diff --git a/tempfork/gliderlabs/ssh/options.go b/tempfork/gliderlabs/ssh/options.go index aa87a4f39..bb24909be 100644 --- a/tempfork/gliderlabs/ssh/options.go +++ b/tempfork/gliderlabs/ssh/options.go @@ -1,84 +1,84 @@ -package ssh - -import ( - "os" - - gossh "github.com/tailscale/golang-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 := os.ReadFile(filepath) - if err != nil { - return err - } - - signer, err := gossh.ParsePrivateKey(pemBytes) - if err != nil { - return err - } - - srv.AddHostKey(signer) - - return nil - } -} - -func KeyboardInteractiveAuth(fn KeyboardInteractiveHandler) Option { - return func(srv *Server) error { - srv.KeyboardInteractiveHandler = fn - 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 - } -} +package ssh
+
+import (
+ "os"
+
+ gossh "github.com/tailscale/golang-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 := os.ReadFile(filepath)
+ if err != nil {
+ return err
+ }
+
+ signer, err := gossh.ParsePrivateKey(pemBytes)
+ if err != nil {
+ return err
+ }
+
+ srv.AddHostKey(signer)
+
+ return nil
+ }
+}
+
+func KeyboardInteractiveAuth(fn KeyboardInteractiveHandler) Option {
+ return func(srv *Server) error {
+ srv.KeyboardInteractiveHandler = fn
+ 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/tempfork/gliderlabs/ssh/options_test.go b/tempfork/gliderlabs/ssh/options_test.go index 7cf6f376c..3aa2f1cf5 100644 --- a/tempfork/gliderlabs/ssh/options_test.go +++ b/tempfork/gliderlabs/ssh/options_test.go @@ -1,111 +1,111 @@ -//go:build glidertests - -package ssh - -import ( - "net" - "strings" - "sync/atomic" - "testing" - - gossh "github.com/tailscale/golang-x-crypto/ssh" -) - -func newTestSessionWithOptions(t *testing.T, srv *Server, cfg *gossh.ClientConfig, options ...Option) (*gossh.Session, *gossh.Client, func()) { - for _, option := range options { - if err := srv.SetOption(option); err != nil { - t.Fatal(err) - } - } - return newTestSession(t, srv, cfg) -} - -func TestPasswordAuth(t *testing.T) { - t.Parallel() - testUser := "testuser" - testPass := "testpass" - session, _, cleanup := newTestSessionWithOptions(t, &Server{ - Handler: func(s Session) { - // noop - }, - }, &gossh.ClientConfig{ - User: testUser, - Auth: []gossh.AuthMethod{ - gossh.Password(testPass), - }, - HostKeyCallback: gossh.InsecureIgnoreHostKey(), - }, PasswordAuth(func(ctx Context, password string) bool { - if ctx.User() != testUser { - t.Fatalf("user = %#v; want %#v", ctx.User(), testUser) - } - if password != testPass { - t.Fatalf("user = %#v; want %#v", password, testPass) - } - return true - })) - defer cleanup() - if err := session.Run(""); err != nil { - t.Fatal(err) - } -} - -func TestPasswordAuthBadPass(t *testing.T) { - t.Parallel() - l := newLocalListener() - srv := &Server{Handler: func(s Session) {}} - srv.SetOption(PasswordAuth(func(ctx Context, password string) bool { - return false - })) - go srv.serveOnce(l) - _, err := gossh.Dial("tcp", l.Addr().String(), &gossh.ClientConfig{ - User: "testuser", - Auth: []gossh.AuthMethod{ - gossh.Password("testpass"), - }, - HostKeyCallback: gossh.InsecureIgnoreHostKey(), - }) - if err != nil { - if !strings.Contains(err.Error(), "unable to authenticate") { - t.Fatal(err) - } - } -} - -type wrappedConn struct { - net.Conn - written int32 -} - -func (c *wrappedConn) Write(p []byte) (n int, err error) { - n, err = c.Conn.Write(p) - atomic.AddInt32(&(c.written), int32(n)) - return -} - -func TestConnWrapping(t *testing.T) { - t.Parallel() - var wrapped *wrappedConn - session, _, cleanup := newTestSessionWithOptions(t, &Server{ - Handler: func(s Session) { - // nothing - }, - }, &gossh.ClientConfig{ - User: "testuser", - Auth: []gossh.AuthMethod{ - gossh.Password("testpass"), - }, - HostKeyCallback: gossh.InsecureIgnoreHostKey(), - }, PasswordAuth(func(ctx Context, password string) bool { - return true - }), WrapConn(func(ctx Context, conn net.Conn) net.Conn { - wrapped = &wrappedConn{conn, 0} - return wrapped - })) - defer cleanup() - if err := session.Shell(); err != nil { - t.Fatal(err) - } - if atomic.LoadInt32(&(wrapped.written)) == 0 { - t.Fatal("wrapped conn not written to") - } -} +//go:build glidertests
+
+package ssh
+
+import (
+ "net"
+ "strings"
+ "sync/atomic"
+ "testing"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+func newTestSessionWithOptions(t *testing.T, srv *Server, cfg *gossh.ClientConfig, options ...Option) (*gossh.Session, *gossh.Client, func()) {
+ for _, option := range options {
+ if err := srv.SetOption(option); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return newTestSession(t, srv, cfg)
+}
+
+func TestPasswordAuth(t *testing.T) {
+ t.Parallel()
+ testUser := "testuser"
+ testPass := "testpass"
+ session, _, cleanup := newTestSessionWithOptions(t, &Server{
+ Handler: func(s Session) {
+ // noop
+ },
+ }, &gossh.ClientConfig{
+ User: testUser,
+ Auth: []gossh.AuthMethod{
+ gossh.Password(testPass),
+ },
+ HostKeyCallback: gossh.InsecureIgnoreHostKey(),
+ }, PasswordAuth(func(ctx Context, password string) bool {
+ if ctx.User() != testUser {
+ t.Fatalf("user = %#v; want %#v", ctx.User(), testUser)
+ }
+ if password != testPass {
+ t.Fatalf("user = %#v; want %#v", password, testPass)
+ }
+ return true
+ }))
+ defer cleanup()
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPasswordAuthBadPass(t *testing.T) {
+ t.Parallel()
+ l := newLocalListener()
+ srv := &Server{Handler: func(s Session) {}}
+ srv.SetOption(PasswordAuth(func(ctx Context, password string) bool {
+ return false
+ }))
+ go srv.serveOnce(l)
+ _, err := gossh.Dial("tcp", l.Addr().String(), &gossh.ClientConfig{
+ User: "testuser",
+ Auth: []gossh.AuthMethod{
+ gossh.Password("testpass"),
+ },
+ HostKeyCallback: gossh.InsecureIgnoreHostKey(),
+ })
+ if err != nil {
+ if !strings.Contains(err.Error(), "unable to authenticate") {
+ t.Fatal(err)
+ }
+ }
+}
+
+type wrappedConn struct {
+ net.Conn
+ written int32
+}
+
+func (c *wrappedConn) Write(p []byte) (n int, err error) {
+ n, err = c.Conn.Write(p)
+ atomic.AddInt32(&(c.written), int32(n))
+ return
+}
+
+func TestConnWrapping(t *testing.T) {
+ t.Parallel()
+ var wrapped *wrappedConn
+ session, _, cleanup := newTestSessionWithOptions(t, &Server{
+ Handler: func(s Session) {
+ // nothing
+ },
+ }, &gossh.ClientConfig{
+ User: "testuser",
+ Auth: []gossh.AuthMethod{
+ gossh.Password("testpass"),
+ },
+ HostKeyCallback: gossh.InsecureIgnoreHostKey(),
+ }, PasswordAuth(func(ctx Context, password string) bool {
+ return true
+ }), WrapConn(func(ctx Context, conn net.Conn) net.Conn {
+ wrapped = &wrappedConn{conn, 0}
+ return wrapped
+ }))
+ defer cleanup()
+ if err := session.Shell(); err != nil {
+ t.Fatal(err)
+ }
+ if atomic.LoadInt32(&(wrapped.written)) == 0 {
+ t.Fatal("wrapped conn not written to")
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/server.go b/tempfork/gliderlabs/ssh/server.go index 1086a72ca..32f633e87 100644 --- a/tempfork/gliderlabs/ssh/server.go +++ b/tempfork/gliderlabs/ssh/server.go @@ -1,459 +1,459 @@ -package ssh - -import ( - "context" - "errors" - "fmt" - "net" - "sync" - "time" - - gossh "github.com/tailscale/golang-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 SubsystemHandler func(s Session) - -var DefaultSubsystemHandlers = map[string]SubsystemHandler{} - -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 - NoClientAuthHandler NoClientAuthHandler // no client 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 - - ConnectionFailedCallback ConnectionFailedCallback // callback to report connection failures - - 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 - - // SubsystemHandlers are handlers which are similar to the usual SSH command - // handlers, but handle named subsystems. - SubsystemHandlers map[string]SubsystemHandler - - listenerWg sync.WaitGroup - mu sync.RWMutex - listeners map[net.Listener]struct{} - conns map[*gossh.ServerConn]struct{} - connWg sync.WaitGroup - doneChan chan struct{} -} - -func (srv *Server) ensureHostSigner() error { - srv.mu.Lock() - defer srv.mu.Unlock() - - 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 - } - } - if srv.SubsystemHandlers == nil { - srv.SubsystemHandlers = map[string]SubsystemHandler{} - for k, v := range DefaultSubsystemHandlers { - srv.SubsystemHandlers[k] = v - } - } -} - -func (srv *Server) config(ctx Context) *gossh.ServerConfig { - srv.mu.RLock() - defer srv.mu.RUnlock() - - 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 && srv.KeyboardInteractiveHandler == 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 err := srv.PublicKeyHandler(ctx, key); err != nil { - return ctx.Permissions().Permissions, err - } - 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) { - applyConnMetadata(ctx, conn) - if ok := srv.KeyboardInteractiveHandler(ctx, challenger); !ok { - return ctx.Permissions().Permissions, fmt.Errorf("permission denied") - } - return ctx.Permissions().Permissions, nil - } - } - if srv.NoClientAuthHandler != nil { - config.NoClientAuthCallback = func(conn gossh.ConnMetadata) (*gossh.Permissions, error) { - applyConnMetadata(ctx, conn) - if err := srv.NoClientAuthHandler(ctx); err != nil { - return ctx.Permissions().Permissions, err - } - return ctx.Permissions().Permissions, nil - } - } - return config -} - -// Handle sets the Handler for the server. -func (srv *Server) Handle(fn Handler) { - srv.mu.Lock() - defer srv.mu.Unlock() - - 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) { - ctx, cancel := newContext(srv) - if srv.ConnCallback != nil { - cbConn := srv.ConnCallback(ctx, newConn) - if cbConn == nil { - newConn.Close() - return - } - newConn = cbConn - } - 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 { - if srv.ConnectionFailedCallback != nil { - srv.ConnectionFailedCallback(conn, err) - } - 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) { - srv.mu.Lock() - defer srv.mu.Unlock() - - // these are later added via AddHostKey on ServerConfig, which performs the - // check for one of every algorithm. - - // This check is based on the AddHostKey method from the x/crypto/ssh - // library. This allows us to only keep one active key for each type on a - // server at once. So, if you're dynamically updating keys at runtime, this - // list will not keep growing. - for i, k := range srv.HostSigners { - if k.PublicKey().Type() == key.PublicKey().Type() { - srv.HostSigners[i] = key - return - } - } - - srv.HostSigners = append(srv.HostSigners, key) -} - -// SetOption runs a functional option against the server. -func (srv *Server) SetOption(option Option) error { - // NOTE: there is a potential race here for any option that doesn't call an - // internal method. We can't actually lock here because if something calls - // (as an example) AddHostKey, it will deadlock. - - //srv.mu.Lock() - //defer srv.mu.Unlock() - - 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() - } -} +package ssh
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "sync"
+ "time"
+
+ gossh "github.com/tailscale/golang-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 SubsystemHandler func(s Session)
+
+var DefaultSubsystemHandlers = map[string]SubsystemHandler{}
+
+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
+ NoClientAuthHandler NoClientAuthHandler // no client 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
+
+ ConnectionFailedCallback ConnectionFailedCallback // callback to report connection failures
+
+ 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
+
+ // SubsystemHandlers are handlers which are similar to the usual SSH command
+ // handlers, but handle named subsystems.
+ SubsystemHandlers map[string]SubsystemHandler
+
+ listenerWg sync.WaitGroup
+ mu sync.RWMutex
+ listeners map[net.Listener]struct{}
+ conns map[*gossh.ServerConn]struct{}
+ connWg sync.WaitGroup
+ doneChan chan struct{}
+}
+
+func (srv *Server) ensureHostSigner() error {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ 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
+ }
+ }
+ if srv.SubsystemHandlers == nil {
+ srv.SubsystemHandlers = map[string]SubsystemHandler{}
+ for k, v := range DefaultSubsystemHandlers {
+ srv.SubsystemHandlers[k] = v
+ }
+ }
+}
+
+func (srv *Server) config(ctx Context) *gossh.ServerConfig {
+ srv.mu.RLock()
+ defer srv.mu.RUnlock()
+
+ 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 && srv.KeyboardInteractiveHandler == 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 err := srv.PublicKeyHandler(ctx, key); err != nil {
+ return ctx.Permissions().Permissions, err
+ }
+ 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) {
+ applyConnMetadata(ctx, conn)
+ if ok := srv.KeyboardInteractiveHandler(ctx, challenger); !ok {
+ return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
+ }
+ return ctx.Permissions().Permissions, nil
+ }
+ }
+ if srv.NoClientAuthHandler != nil {
+ config.NoClientAuthCallback = func(conn gossh.ConnMetadata) (*gossh.Permissions, error) {
+ applyConnMetadata(ctx, conn)
+ if err := srv.NoClientAuthHandler(ctx); err != nil {
+ return ctx.Permissions().Permissions, err
+ }
+ return ctx.Permissions().Permissions, nil
+ }
+ }
+ return config
+}
+
+// Handle sets the Handler for the server.
+func (srv *Server) Handle(fn Handler) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ 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) {
+ ctx, cancel := newContext(srv)
+ if srv.ConnCallback != nil {
+ cbConn := srv.ConnCallback(ctx, newConn)
+ if cbConn == nil {
+ newConn.Close()
+ return
+ }
+ newConn = cbConn
+ }
+ 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 {
+ if srv.ConnectionFailedCallback != nil {
+ srv.ConnectionFailedCallback(conn, err)
+ }
+ 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) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ // these are later added via AddHostKey on ServerConfig, which performs the
+ // check for one of every algorithm.
+
+ // This check is based on the AddHostKey method from the x/crypto/ssh
+ // library. This allows us to only keep one active key for each type on a
+ // server at once. So, if you're dynamically updating keys at runtime, this
+ // list will not keep growing.
+ for i, k := range srv.HostSigners {
+ if k.PublicKey().Type() == key.PublicKey().Type() {
+ srv.HostSigners[i] = key
+ return
+ }
+ }
+
+ srv.HostSigners = append(srv.HostSigners, key)
+}
+
+// SetOption runs a functional option against the server.
+func (srv *Server) SetOption(option Option) error {
+ // NOTE: there is a potential race here for any option that doesn't call an
+ // internal method. We can't actually lock here because if something calls
+ // (as an example) AddHostKey, it will deadlock.
+
+ //srv.mu.Lock()
+ //defer srv.mu.Unlock()
+
+ 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/tempfork/gliderlabs/ssh/server_test.go b/tempfork/gliderlabs/ssh/server_test.go index 177c07117..1a63bb4b2 100644 --- a/tempfork/gliderlabs/ssh/server_test.go +++ b/tempfork/gliderlabs/ssh/server_test.go @@ -1,128 +1,128 @@ -//go:build glidertests - -package ssh - -import ( - "bytes" - "context" - "io" - "testing" - "time" -) - -func TestAddHostKey(t *testing.T) { - s := Server{} - signer, err := generateSigner() - if err != nil { - t.Fatal(err) - } - s.AddHostKey(signer) - if len(s.HostSigners) != 1 { - t.Fatal("Key was not properly added") - } - signer, err = generateSigner() - if err != nil { - t.Fatal(err) - } - s.AddHostKey(signer) - if len(s.HostSigners) != 1 { - t.Fatal("Key was not properly replaced") - } -} - -func TestServerShutdown(t *testing.T) { - l := newLocalListener() - testBytes := []byte("Hello world\n") - s := &Server{ - Handler: func(s Session) { - s.Write(testBytes) - time.Sleep(50 * time.Millisecond) - }, - } - go func() { - err := s.Serve(l) - if err != nil && err != ErrServerClosed { - t.Fatal(err) - } - }() - sessDone := make(chan struct{}) - sess, _, cleanup := newClientSession(t, l.Addr().String(), nil) - go func() { - defer cleanup() - defer close(sessDone) - var stdout bytes.Buffer - sess.Stdout = &stdout - if err := sess.Run(""); err != nil { - t.Fatal(err) - } - if !bytes.Equal(stdout.Bytes(), testBytes) { - t.Fatalf("expected = %s; got %s", testBytes, stdout.Bytes()) - } - }() - - srvDone := make(chan struct{}) - go func() { - defer close(srvDone) - err := s.Shutdown(context.Background()) - if err != nil { - t.Fatal(err) - } - }() - - timeout := time.After(2 * time.Second) - select { - case <-timeout: - t.Fatal("timeout") - return - case <-srvDone: - // TODO: add timeout for sessDone - <-sessDone - return - } -} - -func TestServerClose(t *testing.T) { - l := newLocalListener() - s := &Server{ - Handler: func(s Session) { - time.Sleep(5 * time.Second) - }, - } - go func() { - err := s.Serve(l) - if err != nil && err != ErrServerClosed { - t.Fatal(err) - } - }() - - clientDoneChan := make(chan struct{}) - closeDoneChan := make(chan struct{}) - - sess, _, cleanup := newClientSession(t, l.Addr().String(), nil) - go func() { - defer cleanup() - defer close(clientDoneChan) - <-closeDoneChan - if err := sess.Run(""); err != nil && err != io.EOF { - t.Fatal(err) - } - }() - - go func() { - err := s.Close() - if err != nil { - t.Fatal(err) - } - close(closeDoneChan) - }() - - timeout := time.After(100 * time.Millisecond) - select { - case <-timeout: - t.Error("timeout") - return - case <-s.getDoneChan(): - <-clientDoneChan - return - } -} +//go:build glidertests
+
+package ssh
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "testing"
+ "time"
+)
+
+func TestAddHostKey(t *testing.T) {
+ s := Server{}
+ signer, err := generateSigner()
+ if err != nil {
+ t.Fatal(err)
+ }
+ s.AddHostKey(signer)
+ if len(s.HostSigners) != 1 {
+ t.Fatal("Key was not properly added")
+ }
+ signer, err = generateSigner()
+ if err != nil {
+ t.Fatal(err)
+ }
+ s.AddHostKey(signer)
+ if len(s.HostSigners) != 1 {
+ t.Fatal("Key was not properly replaced")
+ }
+}
+
+func TestServerShutdown(t *testing.T) {
+ l := newLocalListener()
+ testBytes := []byte("Hello world\n")
+ s := &Server{
+ Handler: func(s Session) {
+ s.Write(testBytes)
+ time.Sleep(50 * time.Millisecond)
+ },
+ }
+ go func() {
+ err := s.Serve(l)
+ if err != nil && err != ErrServerClosed {
+ t.Fatal(err)
+ }
+ }()
+ sessDone := make(chan struct{})
+ sess, _, cleanup := newClientSession(t, l.Addr().String(), nil)
+ go func() {
+ defer cleanup()
+ defer close(sessDone)
+ var stdout bytes.Buffer
+ sess.Stdout = &stdout
+ if err := sess.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stdout.Bytes(), testBytes) {
+ t.Fatalf("expected = %s; got %s", testBytes, stdout.Bytes())
+ }
+ }()
+
+ srvDone := make(chan struct{})
+ go func() {
+ defer close(srvDone)
+ err := s.Shutdown(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ }()
+
+ timeout := time.After(2 * time.Second)
+ select {
+ case <-timeout:
+ t.Fatal("timeout")
+ return
+ case <-srvDone:
+ // TODO: add timeout for sessDone
+ <-sessDone
+ return
+ }
+}
+
+func TestServerClose(t *testing.T) {
+ l := newLocalListener()
+ s := &Server{
+ Handler: func(s Session) {
+ time.Sleep(5 * time.Second)
+ },
+ }
+ go func() {
+ err := s.Serve(l)
+ if err != nil && err != ErrServerClosed {
+ t.Fatal(err)
+ }
+ }()
+
+ clientDoneChan := make(chan struct{})
+ closeDoneChan := make(chan struct{})
+
+ sess, _, cleanup := newClientSession(t, l.Addr().String(), nil)
+ go func() {
+ defer cleanup()
+ defer close(clientDoneChan)
+ <-closeDoneChan
+ if err := sess.Run(""); err != nil && err != io.EOF {
+ t.Fatal(err)
+ }
+ }()
+
+ go func() {
+ err := s.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ close(closeDoneChan)
+ }()
+
+ timeout := time.After(100 * time.Millisecond)
+ select {
+ case <-timeout:
+ t.Error("timeout")
+ return
+ case <-s.getDoneChan():
+ <-clientDoneChan
+ return
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/session.go b/tempfork/gliderlabs/ssh/session.go index 0a4a21e53..2f43de739 100644 --- a/tempfork/gliderlabs/ssh/session.go +++ b/tempfork/gliderlabs/ssh/session.go @@ -1,386 +1,386 @@ -package ssh - -import ( - "bytes" - "context" - "errors" - "fmt" - "net" - "sync" - - "github.com/anmitsu/go-shlex" - gossh "github.com/tailscale/golang-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 -// crypto/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 - - // Subsystem returns the subsystem requested by the user. - Subsystem() 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) - - // Break regisers a channel to receive notifications of break requests sent - // from the client. The channel must handle break requests, or it will block - // the request handling loop. Registering nil will unregister the channel. - // During the time that no channel is registered, breaks are ignored. - Break(c chan<- bool) - - // DisablePTYEmulation disables the session's default minimal PTY emulation. - // If you're setting the pty's termios settings from the Pty request, use - // this method to avoid corruption. - // Currently (2022-03-12) the only emulation implemented is NL-to-CRNL translation (`\n`=>`\r\n`). - // A call of DisablePTYEmulation must precede any call to Write. - DisablePTYEmulation() -} - -// 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, - subsystemHandlers: srv.SubsystemHandlers, - ctx: ctx, - } - sess.handleRequests(reqs) -} - -type session struct { - sync.Mutex - gossh.Channel - conn *gossh.ServerConn - handler Handler - subsystemHandlers map[string]SubsystemHandler - handled bool - exited bool - pty *Pty - winch chan Window - env []string - ptyCb PtyCallback - sessReqCb SessionRequestCallback - rawCmd string - subsystem string - ctx Context - sigCh chan<- Signal - sigBuf []Signal - breakCh chan<- bool - disablePtyEmulation bool -} - -func (sess *session) DisablePTYEmulation() { - sess.disablePtyEmulation = true -} - -func (sess *session) Write(p []byte) (n int, err error) { - if sess.pty != nil && !sess.disablePtyEmulation { - 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) Subsystem() string { - return sess.subsystem -} - -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) Break(c chan<- bool) { - sess.Lock() - defer sess.Unlock() - sess.breakCh = c -} - -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 "subsystem": - if sess.handled { - req.Reply(false, nil) - continue - } - - var payload = struct{ Value string }{} - gossh.Unmarshal(req.Payload, &payload) - sess.subsystem = 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 - } - - handler := sess.subsystemHandlers[payload.Value] - if handler == nil { - handler = sess.subsystemHandlers["default"] - } - if handler == nil { - req.Reply(false, nil) - continue - } - - sess.handled = true - req.Reply(true, nil) - - go func() { - 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 := parseWindow(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) - case "break": - ok := false - sess.Lock() - if sess.breakCh != nil { - sess.breakCh <- true - ok = true - } - req.Reply(ok, nil) - sess.Unlock() - default: - // TODO: debug log - req.Reply(false, nil) - } - } -} +package ssh
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "sync"
+
+ "github.com/anmitsu/go-shlex"
+ gossh "github.com/tailscale/golang-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
+// crypto/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
+
+ // Subsystem returns the subsystem requested by the user.
+ Subsystem() 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)
+
+ // Break regisers a channel to receive notifications of break requests sent
+ // from the client. The channel must handle break requests, or it will block
+ // the request handling loop. Registering nil will unregister the channel.
+ // During the time that no channel is registered, breaks are ignored.
+ Break(c chan<- bool)
+
+ // DisablePTYEmulation disables the session's default minimal PTY emulation.
+ // If you're setting the pty's termios settings from the Pty request, use
+ // this method to avoid corruption.
+ // Currently (2022-03-12) the only emulation implemented is NL-to-CRNL translation (`\n`=>`\r\n`).
+ // A call of DisablePTYEmulation must precede any call to Write.
+ DisablePTYEmulation()
+}
+
+// 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,
+ subsystemHandlers: srv.SubsystemHandlers,
+ ctx: ctx,
+ }
+ sess.handleRequests(reqs)
+}
+
+type session struct {
+ sync.Mutex
+ gossh.Channel
+ conn *gossh.ServerConn
+ handler Handler
+ subsystemHandlers map[string]SubsystemHandler
+ handled bool
+ exited bool
+ pty *Pty
+ winch chan Window
+ env []string
+ ptyCb PtyCallback
+ sessReqCb SessionRequestCallback
+ rawCmd string
+ subsystem string
+ ctx Context
+ sigCh chan<- Signal
+ sigBuf []Signal
+ breakCh chan<- bool
+ disablePtyEmulation bool
+}
+
+func (sess *session) DisablePTYEmulation() {
+ sess.disablePtyEmulation = true
+}
+
+func (sess *session) Write(p []byte) (n int, err error) {
+ if sess.pty != nil && !sess.disablePtyEmulation {
+ 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) Subsystem() string {
+ return sess.subsystem
+}
+
+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) Break(c chan<- bool) {
+ sess.Lock()
+ defer sess.Unlock()
+ sess.breakCh = c
+}
+
+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 "subsystem":
+ if sess.handled {
+ req.Reply(false, nil)
+ continue
+ }
+
+ var payload = struct{ Value string }{}
+ gossh.Unmarshal(req.Payload, &payload)
+ sess.subsystem = 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
+ }
+
+ handler := sess.subsystemHandlers[payload.Value]
+ if handler == nil {
+ handler = sess.subsystemHandlers["default"]
+ }
+ if handler == nil {
+ req.Reply(false, nil)
+ continue
+ }
+
+ sess.handled = true
+ req.Reply(true, nil)
+
+ go func() {
+ 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 := parseWindow(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)
+ case "break":
+ ok := false
+ sess.Lock()
+ if sess.breakCh != nil {
+ sess.breakCh <- true
+ ok = true
+ }
+ req.Reply(ok, nil)
+ sess.Unlock()
+ default:
+ // TODO: debug log
+ req.Reply(false, nil)
+ }
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/session_test.go b/tempfork/gliderlabs/ssh/session_test.go index a60be5ec1..fddd67f6d 100644 --- a/tempfork/gliderlabs/ssh/session_test.go +++ b/tempfork/gliderlabs/ssh/session_test.go @@ -1,440 +1,440 @@ -//go:build glidertests - -package ssh - -import ( - "bytes" - "fmt" - "io" - "net" - "testing" - - gossh "github.com/tailscale/golang-x-crypto/ssh" -) - -func (srv *Server) serveOnce(l net.Listener) error { - srv.ensureHandlers() - if err := srv.ensureHostSigner(); err != nil { - return err - } - conn, e := l.Accept() - if e != nil { - return e - } - srv.ChannelHandlers = map[string]ChannelHandler{ - "session": DefaultSessionHandler, - "direct-tcpip": DirectTCPIPHandler, - } - srv.HandleConn(conn) - return nil -} - -func newLocalListener() net.Listener { - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { - panic(fmt.Sprintf("failed to listen on a port: %v", err)) - } - } - return l -} - -func newClientSession(t *testing.T, addr string, config *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) { - if config == nil { - config = &gossh.ClientConfig{ - User: "testuser", - Auth: []gossh.AuthMethod{ - gossh.Password("testpass"), - }, - } - } - if config.HostKeyCallback == nil { - config.HostKeyCallback = gossh.InsecureIgnoreHostKey() - } - client, err := gossh.Dial("tcp", addr, config) - if err != nil { - t.Fatal(err) - } - session, err := client.NewSession() - if err != nil { - t.Fatal(err) - } - return session, client, func() { - session.Close() - client.Close() - } -} - -func newTestSession(t *testing.T, srv *Server, cfg *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) { - l := newLocalListener() - go srv.serveOnce(l) - return newClientSession(t, l.Addr().String(), cfg) -} - -func TestStdout(t *testing.T) { - t.Parallel() - testBytes := []byte("Hello world\n") - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - s.Write(testBytes) - }, - }, nil) - defer cleanup() - var stdout bytes.Buffer - session.Stdout = &stdout - if err := session.Run(""); err != nil { - t.Fatal(err) - } - if !bytes.Equal(stdout.Bytes(), testBytes) { - t.Fatalf("stdout = %#v; want %#v", stdout.Bytes(), testBytes) - } -} - -func TestStderr(t *testing.T) { - t.Parallel() - testBytes := []byte("Hello world\n") - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - s.Stderr().Write(testBytes) - }, - }, nil) - defer cleanup() - var stderr bytes.Buffer - session.Stderr = &stderr - if err := session.Run(""); err != nil { - t.Fatal(err) - } - if !bytes.Equal(stderr.Bytes(), testBytes) { - t.Fatalf("stderr = %#v; want %#v", stderr.Bytes(), testBytes) - } -} - -func TestStdin(t *testing.T) { - t.Parallel() - testBytes := []byte("Hello world\n") - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - io.Copy(s, s) // stdin back into stdout - }, - }, nil) - defer cleanup() - var stdout bytes.Buffer - session.Stdout = &stdout - session.Stdin = bytes.NewBuffer(testBytes) - if err := session.Run(""); err != nil { - t.Fatal(err) - } - if !bytes.Equal(stdout.Bytes(), testBytes) { - t.Fatalf("stdout = %#v; want %#v given stdin = %#v", stdout.Bytes(), testBytes, testBytes) - } -} - -func TestUser(t *testing.T) { - t.Parallel() - testUser := []byte("progrium") - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - io.WriteString(s, s.User()) - }, - }, &gossh.ClientConfig{ - User: string(testUser), - }) - defer cleanup() - var stdout bytes.Buffer - session.Stdout = &stdout - if err := session.Run(""); err != nil { - t.Fatal(err) - } - if !bytes.Equal(stdout.Bytes(), testUser) { - t.Fatalf("stdout = %#v; want %#v given user = %#v", stdout.Bytes(), testUser, string(testUser)) - } -} - -func TestDefaultExitStatusZero(t *testing.T) { - t.Parallel() - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - // noop - }, - }, nil) - defer cleanup() - err := session.Run("") - if err != nil { - t.Fatalf("expected nil but got %v", err) - } -} - -func TestExplicitExitStatusZero(t *testing.T) { - t.Parallel() - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - s.Exit(0) - }, - }, nil) - defer cleanup() - err := session.Run("") - if err != nil { - t.Fatalf("expected nil but got %v", err) - } -} - -func TestExitStatusNonZero(t *testing.T) { - t.Parallel() - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - s.Exit(1) - }, - }, nil) - defer cleanup() - err := session.Run("") - e, ok := err.(*gossh.ExitError) - if !ok { - t.Fatalf("expected ExitError but got %T", err) - } - if e.ExitStatus() != 1 { - t.Fatalf("exit-status = %#v; want %#v", e.ExitStatus(), 1) - } -} - -func TestPty(t *testing.T) { - t.Parallel() - term := "xterm" - winWidth := 40 - winHeight := 80 - done := make(chan bool) - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - ptyReq, _, isPty := s.Pty() - if !isPty { - t.Fatalf("expected pty but none requested") - } - if ptyReq.Term != term { - t.Fatalf("expected term %#v but got %#v", term, ptyReq.Term) - } - if ptyReq.Window.Width != winWidth { - t.Fatalf("expected window width %#v but got %#v", winWidth, ptyReq.Window.Width) - } - if ptyReq.Window.Height != winHeight { - t.Fatalf("expected window height %#v but got %#v", winHeight, ptyReq.Window.Height) - } - close(done) - }, - }, nil) - defer cleanup() - if err := session.RequestPty(term, winHeight, winWidth, gossh.TerminalModes{}); err != nil { - t.Fatalf("expected nil but got %v", err) - } - if err := session.Shell(); err != nil { - t.Fatalf("expected nil but got %v", err) - } - <-done -} - -func TestPtyResize(t *testing.T) { - t.Parallel() - winch0 := Window{Width: 40, Height: 80} - winch1 := Window{Width: 80, Height: 160} - winch2 := Window{Width: 20, Height: 40} - winches := make(chan Window) - done := make(chan bool) - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - ptyReq, winCh, isPty := s.Pty() - if !isPty { - t.Fatalf("expected pty but none requested") - } - if ptyReq.Window != winch0 { - t.Fatalf("expected window %#v but got %#v", winch0, ptyReq.Window) - } - for win := range winCh { - winches <- win - } - close(done) - }, - }, nil) - defer cleanup() - // winch0 - if err := session.RequestPty("xterm", winch0.Height, winch0.Width, gossh.TerminalModes{}); err != nil { - t.Fatalf("expected nil but got %v", err) - } - if err := session.Shell(); err != nil { - t.Fatalf("expected nil but got %v", err) - } - gotWinch := <-winches - if gotWinch != winch0 { - t.Fatalf("expected window %#v but got %#v", winch0, gotWinch) - } - // winch1 - winchMsg := struct{ w, h uint32 }{uint32(winch1.Width), uint32(winch1.Height)} - ok, err := session.SendRequest("window-change", true, gossh.Marshal(&winchMsg)) - if err == nil && !ok { - t.Fatalf("unexpected error or bad reply on send request") - } - gotWinch = <-winches - if gotWinch != winch1 { - t.Fatalf("expected window %#v but got %#v", winch1, gotWinch) - } - // winch2 - winchMsg = struct{ w, h uint32 }{uint32(winch2.Width), uint32(winch2.Height)} - ok, err = session.SendRequest("window-change", true, gossh.Marshal(&winchMsg)) - if err == nil && !ok { - t.Fatalf("unexpected error or bad reply on send request") - } - gotWinch = <-winches - if gotWinch != winch2 { - t.Fatalf("expected window %#v but got %#v", winch2, gotWinch) - } - session.Close() - <-done -} - -func TestSignals(t *testing.T) { - t.Parallel() - - // errChan lets us get errors back from the session - errChan := make(chan error, 5) - - // doneChan lets us specify that we should exit. - doneChan := make(chan interface{}) - - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - // We need to use a buffered channel here, otherwise it's possible for the - // second call to Signal to get discarded. - signals := make(chan Signal, 2) - s.Signals(signals) - - select { - case sig := <-signals: - if sig != SIGINT { - errChan <- fmt.Errorf("expected signal %v but got %v", SIGINT, sig) - return - } - case <-doneChan: - errChan <- fmt.Errorf("Unexpected done") - return - } - - select { - case sig := <-signals: - if sig != SIGKILL { - errChan <- fmt.Errorf("expected signal %v but got %v", SIGKILL, sig) - return - } - case <-doneChan: - errChan <- fmt.Errorf("Unexpected done") - return - } - }, - }, nil) - defer cleanup() - - go func() { - session.Signal(gossh.SIGINT) - session.Signal(gossh.SIGKILL) - }() - - go func() { - errChan <- session.Run("") - }() - - err := <-errChan - close(doneChan) - - if err != nil { - t.Fatalf("expected nil but got %v", err) - } -} - -func TestBreakWithChanRegistered(t *testing.T) { - t.Parallel() - - // errChan lets us get errors back from the session - errChan := make(chan error, 5) - - // doneChan lets us specify that we should exit. - doneChan := make(chan interface{}) - - breakChan := make(chan bool) - - readyToReceiveBreak := make(chan bool) - - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - s.Break(breakChan) // register a break channel with the session - readyToReceiveBreak <- true - - select { - case <-breakChan: - io.WriteString(s, "break") - case <-doneChan: - errChan <- fmt.Errorf("Unexpected done") - return - } - }, - }, nil) - defer cleanup() - var stdout bytes.Buffer - session.Stdout = &stdout - go func() { - errChan <- session.Run("") - }() - - <-readyToReceiveBreak - ok, err := session.SendRequest("break", true, nil) - if err != nil { - t.Fatalf("expected nil but got %v", err) - } - if ok != true { - t.Fatalf("expected true but got %v", ok) - } - - err = <-errChan - close(doneChan) - - if err != nil { - t.Fatalf("expected nil but got %v", err) - } - if !bytes.Equal(stdout.Bytes(), []byte("break")) { - t.Fatalf("stdout = %#v, expected 'break'", stdout.Bytes()) - } -} - -func TestBreakWithoutChanRegistered(t *testing.T) { - t.Parallel() - - // errChan lets us get errors back from the session - errChan := make(chan error, 5) - - // doneChan lets us specify that we should exit. - doneChan := make(chan interface{}) - - waitUntilAfterBreakSent := make(chan bool) - - session, _, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) { - <-waitUntilAfterBreakSent - }, - }, nil) - defer cleanup() - var stdout bytes.Buffer - session.Stdout = &stdout - go func() { - errChan <- session.Run("") - }() - - ok, err := session.SendRequest("break", true, nil) - if err != nil { - t.Fatalf("expected nil but got %v", err) - } - if ok != false { - t.Fatalf("expected false but got %v", ok) - } - waitUntilAfterBreakSent <- true - - err = <-errChan - close(doneChan) - if err != nil { - t.Fatalf("expected nil but got %v", err) - } -} +//go:build glidertests
+
+package ssh
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net"
+ "testing"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+func (srv *Server) serveOnce(l net.Listener) error {
+ srv.ensureHandlers()
+ if err := srv.ensureHostSigner(); err != nil {
+ return err
+ }
+ conn, e := l.Accept()
+ if e != nil {
+ return e
+ }
+ srv.ChannelHandlers = map[string]ChannelHandler{
+ "session": DefaultSessionHandler,
+ "direct-tcpip": DirectTCPIPHandler,
+ }
+ srv.HandleConn(conn)
+ return nil
+}
+
+func newLocalListener() net.Listener {
+ l, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
+ panic(fmt.Sprintf("failed to listen on a port: %v", err))
+ }
+ }
+ return l
+}
+
+func newClientSession(t *testing.T, addr string, config *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) {
+ if config == nil {
+ config = &gossh.ClientConfig{
+ User: "testuser",
+ Auth: []gossh.AuthMethod{
+ gossh.Password("testpass"),
+ },
+ }
+ }
+ if config.HostKeyCallback == nil {
+ config.HostKeyCallback = gossh.InsecureIgnoreHostKey()
+ }
+ client, err := gossh.Dial("tcp", addr, config)
+ if err != nil {
+ t.Fatal(err)
+ }
+ session, err := client.NewSession()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return session, client, func() {
+ session.Close()
+ client.Close()
+ }
+}
+
+func newTestSession(t *testing.T, srv *Server, cfg *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) {
+ l := newLocalListener()
+ go srv.serveOnce(l)
+ return newClientSession(t, l.Addr().String(), cfg)
+}
+
+func TestStdout(t *testing.T) {
+ t.Parallel()
+ testBytes := []byte("Hello world\n")
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Write(testBytes)
+ },
+ }, nil)
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stdout.Bytes(), testBytes) {
+ t.Fatalf("stdout = %#v; want %#v", stdout.Bytes(), testBytes)
+ }
+}
+
+func TestStderr(t *testing.T) {
+ t.Parallel()
+ testBytes := []byte("Hello world\n")
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Stderr().Write(testBytes)
+ },
+ }, nil)
+ defer cleanup()
+ var stderr bytes.Buffer
+ session.Stderr = &stderr
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stderr.Bytes(), testBytes) {
+ t.Fatalf("stderr = %#v; want %#v", stderr.Bytes(), testBytes)
+ }
+}
+
+func TestStdin(t *testing.T) {
+ t.Parallel()
+ testBytes := []byte("Hello world\n")
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ io.Copy(s, s) // stdin back into stdout
+ },
+ }, nil)
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ session.Stdin = bytes.NewBuffer(testBytes)
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stdout.Bytes(), testBytes) {
+ t.Fatalf("stdout = %#v; want %#v given stdin = %#v", stdout.Bytes(), testBytes, testBytes)
+ }
+}
+
+func TestUser(t *testing.T) {
+ t.Parallel()
+ testUser := []byte("progrium")
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ io.WriteString(s, s.User())
+ },
+ }, &gossh.ClientConfig{
+ User: string(testUser),
+ })
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stdout.Bytes(), testUser) {
+ t.Fatalf("stdout = %#v; want %#v given user = %#v", stdout.Bytes(), testUser, string(testUser))
+ }
+}
+
+func TestDefaultExitStatusZero(t *testing.T) {
+ t.Parallel()
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ // noop
+ },
+ }, nil)
+ defer cleanup()
+ err := session.Run("")
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+}
+
+func TestExplicitExitStatusZero(t *testing.T) {
+ t.Parallel()
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Exit(0)
+ },
+ }, nil)
+ defer cleanup()
+ err := session.Run("")
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+}
+
+func TestExitStatusNonZero(t *testing.T) {
+ t.Parallel()
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Exit(1)
+ },
+ }, nil)
+ defer cleanup()
+ err := session.Run("")
+ e, ok := err.(*gossh.ExitError)
+ if !ok {
+ t.Fatalf("expected ExitError but got %T", err)
+ }
+ if e.ExitStatus() != 1 {
+ t.Fatalf("exit-status = %#v; want %#v", e.ExitStatus(), 1)
+ }
+}
+
+func TestPty(t *testing.T) {
+ t.Parallel()
+ term := "xterm"
+ winWidth := 40
+ winHeight := 80
+ done := make(chan bool)
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ ptyReq, _, isPty := s.Pty()
+ if !isPty {
+ t.Fatalf("expected pty but none requested")
+ }
+ if ptyReq.Term != term {
+ t.Fatalf("expected term %#v but got %#v", term, ptyReq.Term)
+ }
+ if ptyReq.Window.Width != winWidth {
+ t.Fatalf("expected window width %#v but got %#v", winWidth, ptyReq.Window.Width)
+ }
+ if ptyReq.Window.Height != winHeight {
+ t.Fatalf("expected window height %#v but got %#v", winHeight, ptyReq.Window.Height)
+ }
+ close(done)
+ },
+ }, nil)
+ defer cleanup()
+ if err := session.RequestPty(term, winHeight, winWidth, gossh.TerminalModes{}); err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if err := session.Shell(); err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ <-done
+}
+
+func TestPtyResize(t *testing.T) {
+ t.Parallel()
+ winch0 := Window{Width: 40, Height: 80}
+ winch1 := Window{Width: 80, Height: 160}
+ winch2 := Window{Width: 20, Height: 40}
+ winches := make(chan Window)
+ done := make(chan bool)
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ ptyReq, winCh, isPty := s.Pty()
+ if !isPty {
+ t.Fatalf("expected pty but none requested")
+ }
+ if ptyReq.Window != winch0 {
+ t.Fatalf("expected window %#v but got %#v", winch0, ptyReq.Window)
+ }
+ for win := range winCh {
+ winches <- win
+ }
+ close(done)
+ },
+ }, nil)
+ defer cleanup()
+ // winch0
+ if err := session.RequestPty("xterm", winch0.Height, winch0.Width, gossh.TerminalModes{}); err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if err := session.Shell(); err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ gotWinch := <-winches
+ if gotWinch != winch0 {
+ t.Fatalf("expected window %#v but got %#v", winch0, gotWinch)
+ }
+ // winch1
+ winchMsg := struct{ w, h uint32 }{uint32(winch1.Width), uint32(winch1.Height)}
+ ok, err := session.SendRequest("window-change", true, gossh.Marshal(&winchMsg))
+ if err == nil && !ok {
+ t.Fatalf("unexpected error or bad reply on send request")
+ }
+ gotWinch = <-winches
+ if gotWinch != winch1 {
+ t.Fatalf("expected window %#v but got %#v", winch1, gotWinch)
+ }
+ // winch2
+ winchMsg = struct{ w, h uint32 }{uint32(winch2.Width), uint32(winch2.Height)}
+ ok, err = session.SendRequest("window-change", true, gossh.Marshal(&winchMsg))
+ if err == nil && !ok {
+ t.Fatalf("unexpected error or bad reply on send request")
+ }
+ gotWinch = <-winches
+ if gotWinch != winch2 {
+ t.Fatalf("expected window %#v but got %#v", winch2, gotWinch)
+ }
+ session.Close()
+ <-done
+}
+
+func TestSignals(t *testing.T) {
+ t.Parallel()
+
+ // errChan lets us get errors back from the session
+ errChan := make(chan error, 5)
+
+ // doneChan lets us specify that we should exit.
+ doneChan := make(chan interface{})
+
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ // We need to use a buffered channel here, otherwise it's possible for the
+ // second call to Signal to get discarded.
+ signals := make(chan Signal, 2)
+ s.Signals(signals)
+
+ select {
+ case sig := <-signals:
+ if sig != SIGINT {
+ errChan <- fmt.Errorf("expected signal %v but got %v", SIGINT, sig)
+ return
+ }
+ case <-doneChan:
+ errChan <- fmt.Errorf("Unexpected done")
+ return
+ }
+
+ select {
+ case sig := <-signals:
+ if sig != SIGKILL {
+ errChan <- fmt.Errorf("expected signal %v but got %v", SIGKILL, sig)
+ return
+ }
+ case <-doneChan:
+ errChan <- fmt.Errorf("Unexpected done")
+ return
+ }
+ },
+ }, nil)
+ defer cleanup()
+
+ go func() {
+ session.Signal(gossh.SIGINT)
+ session.Signal(gossh.SIGKILL)
+ }()
+
+ go func() {
+ errChan <- session.Run("")
+ }()
+
+ err := <-errChan
+ close(doneChan)
+
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+}
+
+func TestBreakWithChanRegistered(t *testing.T) {
+ t.Parallel()
+
+ // errChan lets us get errors back from the session
+ errChan := make(chan error, 5)
+
+ // doneChan lets us specify that we should exit.
+ doneChan := make(chan interface{})
+
+ breakChan := make(chan bool)
+
+ readyToReceiveBreak := make(chan bool)
+
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Break(breakChan) // register a break channel with the session
+ readyToReceiveBreak <- true
+
+ select {
+ case <-breakChan:
+ io.WriteString(s, "break")
+ case <-doneChan:
+ errChan <- fmt.Errorf("Unexpected done")
+ return
+ }
+ },
+ }, nil)
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ go func() {
+ errChan <- session.Run("")
+ }()
+
+ <-readyToReceiveBreak
+ ok, err := session.SendRequest("break", true, nil)
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if ok != true {
+ t.Fatalf("expected true but got %v", ok)
+ }
+
+ err = <-errChan
+ close(doneChan)
+
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if !bytes.Equal(stdout.Bytes(), []byte("break")) {
+ t.Fatalf("stdout = %#v, expected 'break'", stdout.Bytes())
+ }
+}
+
+func TestBreakWithoutChanRegistered(t *testing.T) {
+ t.Parallel()
+
+ // errChan lets us get errors back from the session
+ errChan := make(chan error, 5)
+
+ // doneChan lets us specify that we should exit.
+ doneChan := make(chan interface{})
+
+ waitUntilAfterBreakSent := make(chan bool)
+
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ <-waitUntilAfterBreakSent
+ },
+ }, nil)
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ go func() {
+ errChan <- session.Run("")
+ }()
+
+ ok, err := session.SendRequest("break", true, nil)
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if ok != false {
+ t.Fatalf("expected false but got %v", ok)
+ }
+ waitUntilAfterBreakSent <- true
+
+ err = <-errChan
+ close(doneChan)
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/ssh.go b/tempfork/gliderlabs/ssh/ssh.go index 644cb257d..4216ea97a 100644 --- a/tempfork/gliderlabs/ssh/ssh.go +++ b/tempfork/gliderlabs/ssh/ssh.go @@ -1,156 +1,156 @@ -package ssh - -import ( - "crypto/subtle" - "net" - - gossh "github.com/tailscale/golang-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) error - -type NoClientAuthHandler func(ctx Context) error - -type BannerHandler func(ctx Context) string - -// 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(ctx Context, 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 - -// ConnectionFailedCallback is a hook for reporting failed connections -// Please note: the net.Conn is likely to be closed at this point -type ConnectionFailedCallback func(conn net.Conn, err error) - -// Window represents the size of a PTY window. -// -// See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2 -// -// Zero dimension parameters MUST be ignored. The character/row dimensions -// override the pixel dimensions (when nonzero). Pixel dimensions refer -// to the drawable area of the window. -type Window struct { - // Width is the number of columns. - // It overrides WidthPixels. - Width int - // Height is the number of rows. - // It overrides HeightPixels. - Height int - - // WidthPixels is the drawable width of the window, in pixels. - WidthPixels int - // HeightPixels is the drawable height of the window, in pixels. - HeightPixels int -} - -// Pty represents a PTY request and configuration. -type Pty struct { - // Term is the TERM environment variable value. - Term string - - // Window is the Window sent as part of the pty-req. - Window Window - - // Modes represent a mapping of Terminal Mode opcode to value as it was - // requested by the client as part of the pty-req. These are outlined as - // part of https://datatracker.ietf.org/doc/html/rfc4254#section-8. - // - // The opcodes are defined as constants in github.com/tailscale/golang-x-crypto/ssh (VINTR,VQUIT,etc.). - // Boolean opcodes have values 0 or 1. - Modes gossh.TerminalModes -} - -// 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) -} +package ssh
+
+import (
+ "crypto/subtle"
+ "net"
+
+ gossh "github.com/tailscale/golang-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) error
+
+type NoClientAuthHandler func(ctx Context) error
+
+type BannerHandler func(ctx Context) string
+
+// 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(ctx Context, 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
+
+// ConnectionFailedCallback is a hook for reporting failed connections
+// Please note: the net.Conn is likely to be closed at this point
+type ConnectionFailedCallback func(conn net.Conn, err error)
+
+// Window represents the size of a PTY window.
+//
+// See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2
+//
+// Zero dimension parameters MUST be ignored. The character/row dimensions
+// override the pixel dimensions (when nonzero). Pixel dimensions refer
+// to the drawable area of the window.
+type Window struct {
+ // Width is the number of columns.
+ // It overrides WidthPixels.
+ Width int
+ // Height is the number of rows.
+ // It overrides HeightPixels.
+ Height int
+
+ // WidthPixels is the drawable width of the window, in pixels.
+ WidthPixels int
+ // HeightPixels is the drawable height of the window, in pixels.
+ HeightPixels int
+}
+
+// Pty represents a PTY request and configuration.
+type Pty struct {
+ // Term is the TERM environment variable value.
+ Term string
+
+ // Window is the Window sent as part of the pty-req.
+ Window Window
+
+ // Modes represent a mapping of Terminal Mode opcode to value as it was
+ // requested by the client as part of the pty-req. These are outlined as
+ // part of https://datatracker.ietf.org/doc/html/rfc4254#section-8.
+ //
+ // The opcodes are defined as constants in github.com/tailscale/golang-x-crypto/ssh (VINTR,VQUIT,etc.).
+ // Boolean opcodes have values 0 or 1.
+ Modes gossh.TerminalModes
+}
+
+// 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/tempfork/gliderlabs/ssh/ssh_test.go b/tempfork/gliderlabs/ssh/ssh_test.go index aa301b048..8772c03ad 100644 --- a/tempfork/gliderlabs/ssh/ssh_test.go +++ b/tempfork/gliderlabs/ssh/ssh_test.go @@ -1,17 +1,17 @@ -package ssh - -import ( - "testing" -) - -func TestKeysEqual(t *testing.T) { - defer func() { - if r := recover(); r != nil { - t.Errorf("The code did panic") - } - }() - - if KeysEqual(nil, nil) { - t.Error("two nil keys should not return true") - } -} +package ssh
+
+import (
+ "testing"
+)
+
+func TestKeysEqual(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("The code did panic")
+ }
+ }()
+
+ if KeysEqual(nil, nil) {
+ t.Error("two nil keys should not return true")
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/tcpip.go b/tempfork/gliderlabs/ssh/tcpip.go index 056a0c734..d30bb15ac 100644 --- a/tempfork/gliderlabs/ssh/tcpip.go +++ b/tempfork/gliderlabs/ssh/tcpip.go @@ -1,193 +1,193 @@ -package ssh - -import ( - "io" - "log" - "net" - "strconv" - "sync" - - gossh "github.com/tailscale/golang-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 - } -} +package ssh
+
+import (
+ "io"
+ "log"
+ "net"
+ "strconv"
+ "sync"
+
+ gossh "github.com/tailscale/golang-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/tempfork/gliderlabs/ssh/tcpip_test.go b/tempfork/gliderlabs/ssh/tcpip_test.go index 118b5d53a..e1d74d566 100644 --- a/tempfork/gliderlabs/ssh/tcpip_test.go +++ b/tempfork/gliderlabs/ssh/tcpip_test.go @@ -1,85 +1,85 @@ -//go:build glidertests - -package ssh - -import ( - "bytes" - "io" - "net" - "strconv" - "strings" - "testing" - - gossh "github.com/tailscale/golang-x-crypto/ssh" -) - -var sampleServerResponse = []byte("Hello world") - -func sampleSocketServer() net.Listener { - l := newLocalListener() - - go func() { - conn, err := l.Accept() - if err != nil { - return - } - conn.Write(sampleServerResponse) - conn.Close() - }() - - return l -} - -func newTestSessionWithForwarding(t *testing.T, forwardingEnabled bool) (net.Listener, *gossh.Client, func()) { - l := sampleSocketServer() - - _, client, cleanup := newTestSession(t, &Server{ - Handler: func(s Session) {}, - LocalPortForwardingCallback: func(ctx Context, destinationHost string, destinationPort uint32) bool { - addr := net.JoinHostPort(destinationHost, strconv.FormatInt(int64(destinationPort), 10)) - if addr != l.Addr().String() { - panic("unexpected destinationHost: " + addr) - } - return forwardingEnabled - }, - }, nil) - - return l, client, func() { - cleanup() - l.Close() - } -} - -func TestLocalPortForwardingWorks(t *testing.T) { - t.Parallel() - - l, client, cleanup := newTestSessionWithForwarding(t, true) - defer cleanup() - - conn, err := client.Dial("tcp", l.Addr().String()) - if err != nil { - t.Fatalf("Error connecting to %v: %v", l.Addr().String(), err) - } - result, err := io.ReadAll(conn) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(result, sampleServerResponse) { - t.Fatalf("result = %#v; want %#v", result, sampleServerResponse) - } -} - -func TestLocalPortForwardingRespectsCallback(t *testing.T) { - t.Parallel() - - l, client, cleanup := newTestSessionWithForwarding(t, false) - defer cleanup() - - _, err := client.Dial("tcp", l.Addr().String()) - if err == nil { - t.Fatalf("Expected error connecting to %v but it succeeded", l.Addr().String()) - } - if !strings.Contains(err.Error(), "port forwarding is disabled") { - t.Fatalf("Expected permission error but got %#v", err) - } -} +//go:build glidertests
+
+package ssh
+
+import (
+ "bytes"
+ "io"
+ "net"
+ "strconv"
+ "strings"
+ "testing"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+var sampleServerResponse = []byte("Hello world")
+
+func sampleSocketServer() net.Listener {
+ l := newLocalListener()
+
+ go func() {
+ conn, err := l.Accept()
+ if err != nil {
+ return
+ }
+ conn.Write(sampleServerResponse)
+ conn.Close()
+ }()
+
+ return l
+}
+
+func newTestSessionWithForwarding(t *testing.T, forwardingEnabled bool) (net.Listener, *gossh.Client, func()) {
+ l := sampleSocketServer()
+
+ _, client, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {},
+ LocalPortForwardingCallback: func(ctx Context, destinationHost string, destinationPort uint32) bool {
+ addr := net.JoinHostPort(destinationHost, strconv.FormatInt(int64(destinationPort), 10))
+ if addr != l.Addr().String() {
+ panic("unexpected destinationHost: " + addr)
+ }
+ return forwardingEnabled
+ },
+ }, nil)
+
+ return l, client, func() {
+ cleanup()
+ l.Close()
+ }
+}
+
+func TestLocalPortForwardingWorks(t *testing.T) {
+ t.Parallel()
+
+ l, client, cleanup := newTestSessionWithForwarding(t, true)
+ defer cleanup()
+
+ conn, err := client.Dial("tcp", l.Addr().String())
+ if err != nil {
+ t.Fatalf("Error connecting to %v: %v", l.Addr().String(), err)
+ }
+ result, err := io.ReadAll(conn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(result, sampleServerResponse) {
+ t.Fatalf("result = %#v; want %#v", result, sampleServerResponse)
+ }
+}
+
+func TestLocalPortForwardingRespectsCallback(t *testing.T) {
+ t.Parallel()
+
+ l, client, cleanup := newTestSessionWithForwarding(t, false)
+ defer cleanup()
+
+ _, err := client.Dial("tcp", l.Addr().String())
+ if err == nil {
+ t.Fatalf("Expected error connecting to %v but it succeeded", l.Addr().String())
+ }
+ if !strings.Contains(err.Error(), "port forwarding is disabled") {
+ t.Fatalf("Expected permission error but got %#v", err)
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/util.go b/tempfork/gliderlabs/ssh/util.go index e3b5716a3..7a6a18241 100644 --- a/tempfork/gliderlabs/ssh/util.go +++ b/tempfork/gliderlabs/ssh/util.go @@ -1,157 +1,157 @@ -package ssh - -import ( - "crypto/rand" - "crypto/rsa" - "encoding/binary" - - "github.com/tailscale/golang-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(payload []byte) (pty Pty, ok bool) { - // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2 - // 6.2. Requesting a Pseudo-Terminal - // A pseudo-terminal can be allocated for the session by sending the - // following message. - // byte SSH_MSG_CHANNEL_REQUEST - // uint32 recipient channel - // string "pty-req" - // boolean want_reply - // string TERM environment variable value (e.g., vt100) - // uint32 terminal width, characters (e.g., 80) - // uint32 terminal height, rows (e.g., 24) - // uint32 terminal width, pixels (e.g., 640) - // uint32 terminal height, pixels (e.g., 480) - // string encoded terminal modes - - // The payload starts from the TERM variable. - term, rem, ok := parseString(payload) - if !ok { - return - } - win, rem, ok := parseWindow(rem) - if !ok { - return - } - modes, ok := parseTerminalModes(rem) - if !ok { - return - } - pty = Pty{ - Term: term, - Window: win, - Modes: modes, - } - return -} - -func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) { - // See https://datatracker.ietf.org/doc/html/rfc4254#section-8 - // 8. Encoding of Terminal Modes - // - // All 'encoded terminal modes' (as passed in a pty request) are encoded - // into a byte stream. It is intended that the coding be portable - // across different environments. The stream consists of opcode- - // argument pairs wherein the opcode is a byte value. Opcodes 1 to 159 - // have a single uint32 argument. Opcodes 160 to 255 are not yet - // defined, and cause parsing to stop (they should only be used after - // any other data). The stream is terminated by opcode TTY_OP_END - // (0x00). - // - // The client SHOULD put any modes it knows about in the stream, and the - // server MAY ignore any modes it does not know about. This allows some - // degree of machine-independence, at least between systems that use a - // POSIX-like tty interface. The protocol can support other systems as - // well, but the client may need to fill reasonable values for a number - // of parameters so the server pty gets set to a reasonable mode (the - // server leaves all unspecified mode bits in their default values, and - // only some combinations make sense). - _, rem, ok := parseUint32(in) - if !ok { - return - } - const ttyOpEnd = 0 - for len(rem) > 0 { - if modes == nil { - modes = make(ssh.TerminalModes) - } - code := uint8(rem[0]) - rem = rem[1:] - if code == ttyOpEnd || code > 160 { - break - } - var val uint32 - val, rem, ok = parseUint32(rem) - if !ok { - return - } - modes[code] = val - } - ok = true - return -} - -func parseWindow(s []byte) (win Window, rem []byte, ok bool) { - // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.7 - // 6.7. Window Dimension Change Message - // When the window (terminal) size changes on the client side, it MAY - // send a message to the other side to inform it of the new dimensions. - - // byte SSH_MSG_CHANNEL_REQUEST - // uint32 recipient channel - // string "window-change" - // boolean FALSE - // uint32 terminal width, columns - // uint32 terminal height, rows - // uint32 terminal width, pixels - // uint32 terminal height, pixels - wCols, rem, ok := parseUint32(s) - if !ok { - return - } - hRows, rem, ok := parseUint32(rem) - if !ok { - return - } - wPixels, rem, ok := parseUint32(rem) - if !ok { - return - } - hPixels, rem, ok := parseUint32(rem) - if !ok { - return - } - win = Window{ - Width: int(wCols), - Height: int(hRows), - WidthPixels: int(wPixels), - HeightPixels: int(hPixels), - } - return -} - -func parseString(in []byte) (out string, rem []byte, ok bool) { - length, rem, ok := parseUint32(in) - if uint32(len(rem)) < length || !ok { - ok = false - return - } - out, rem = string(rem[:length]), rem[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 -} +package ssh
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/binary"
+
+ "github.com/tailscale/golang-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(payload []byte) (pty Pty, ok bool) {
+ // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2
+ // 6.2. Requesting a Pseudo-Terminal
+ // A pseudo-terminal can be allocated for the session by sending the
+ // following message.
+ // byte SSH_MSG_CHANNEL_REQUEST
+ // uint32 recipient channel
+ // string "pty-req"
+ // boolean want_reply
+ // string TERM environment variable value (e.g., vt100)
+ // uint32 terminal width, characters (e.g., 80)
+ // uint32 terminal height, rows (e.g., 24)
+ // uint32 terminal width, pixels (e.g., 640)
+ // uint32 terminal height, pixels (e.g., 480)
+ // string encoded terminal modes
+
+ // The payload starts from the TERM variable.
+ term, rem, ok := parseString(payload)
+ if !ok {
+ return
+ }
+ win, rem, ok := parseWindow(rem)
+ if !ok {
+ return
+ }
+ modes, ok := parseTerminalModes(rem)
+ if !ok {
+ return
+ }
+ pty = Pty{
+ Term: term,
+ Window: win,
+ Modes: modes,
+ }
+ return
+}
+
+func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) {
+ // See https://datatracker.ietf.org/doc/html/rfc4254#section-8
+ // 8. Encoding of Terminal Modes
+ //
+ // All 'encoded terminal modes' (as passed in a pty request) are encoded
+ // into a byte stream. It is intended that the coding be portable
+ // across different environments. The stream consists of opcode-
+ // argument pairs wherein the opcode is a byte value. Opcodes 1 to 159
+ // have a single uint32 argument. Opcodes 160 to 255 are not yet
+ // defined, and cause parsing to stop (they should only be used after
+ // any other data). The stream is terminated by opcode TTY_OP_END
+ // (0x00).
+ //
+ // The client SHOULD put any modes it knows about in the stream, and the
+ // server MAY ignore any modes it does not know about. This allows some
+ // degree of machine-independence, at least between systems that use a
+ // POSIX-like tty interface. The protocol can support other systems as
+ // well, but the client may need to fill reasonable values for a number
+ // of parameters so the server pty gets set to a reasonable mode (the
+ // server leaves all unspecified mode bits in their default values, and
+ // only some combinations make sense).
+ _, rem, ok := parseUint32(in)
+ if !ok {
+ return
+ }
+ const ttyOpEnd = 0
+ for len(rem) > 0 {
+ if modes == nil {
+ modes = make(ssh.TerminalModes)
+ }
+ code := uint8(rem[0])
+ rem = rem[1:]
+ if code == ttyOpEnd || code > 160 {
+ break
+ }
+ var val uint32
+ val, rem, ok = parseUint32(rem)
+ if !ok {
+ return
+ }
+ modes[code] = val
+ }
+ ok = true
+ return
+}
+
+func parseWindow(s []byte) (win Window, rem []byte, ok bool) {
+ // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.7
+ // 6.7. Window Dimension Change Message
+ // When the window (terminal) size changes on the client side, it MAY
+ // send a message to the other side to inform it of the new dimensions.
+
+ // byte SSH_MSG_CHANNEL_REQUEST
+ // uint32 recipient channel
+ // string "window-change"
+ // boolean FALSE
+ // uint32 terminal width, columns
+ // uint32 terminal height, rows
+ // uint32 terminal width, pixels
+ // uint32 terminal height, pixels
+ wCols, rem, ok := parseUint32(s)
+ if !ok {
+ return
+ }
+ hRows, rem, ok := parseUint32(rem)
+ if !ok {
+ return
+ }
+ wPixels, rem, ok := parseUint32(rem)
+ if !ok {
+ return
+ }
+ hPixels, rem, ok := parseUint32(rem)
+ if !ok {
+ return
+ }
+ win = Window{
+ Width: int(wCols),
+ Height: int(hRows),
+ WidthPixels: int(wPixels),
+ HeightPixels: int(hPixels),
+ }
+ return
+}
+
+func parseString(in []byte) (out string, rem []byte, ok bool) {
+ length, rem, ok := parseUint32(in)
+ if uint32(len(rem)) < length || !ok {
+ ok = false
+ return
+ }
+ out, rem = string(rem[:length]), rem[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/tempfork/gliderlabs/ssh/wrap.go b/tempfork/gliderlabs/ssh/wrap.go index 17867d751..f44f5d9bf 100644 --- a/tempfork/gliderlabs/ssh/wrap.go +++ b/tempfork/gliderlabs/ssh/wrap.go @@ -1,33 +1,33 @@ -package ssh - -import gossh "github.com/tailscale/golang-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) -} +package ssh
+
+import gossh "github.com/tailscale/golang-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)
+}
diff --git a/tempfork/heap/heap.go b/tempfork/heap/heap.go index 3dfab492a..080b80ca5 100644 --- a/tempfork/heap/heap.go +++ b/tempfork/heap/heap.go @@ -1,121 +1,121 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package heap provides heap operations for any type that implements -// heap.Interface. A heap is a tree with the property that each node is the -// minimum-valued node in its subtree. -// -// The minimum element in the tree is the root, at index 0. -// -// A heap is a common way to implement a priority queue. To build a priority -// queue, implement the Heap interface with the (negative) priority as the -// ordering for the Less method, so Push adds items while Pop removes the -// highest-priority item from the queue. The Examples include such an -// implementation; the file example_pq_test.go has the complete source. -// -// This package is a copy of the Go standard library's -// container/heap, but using generics. -package heap - -import "sort" - -// The Interface type describes the requirements -// for a type using the routines in this package. -// Any type that implements it may be used as a -// min-heap with the following invariants (established after -// Init has been called or if the data is empty or sorted): -// -// !h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len() -// -// Note that Push and Pop in this interface are for package heap's -// implementation to call. To add and remove things from the heap, -// use heap.Push and heap.Pop. -type Interface[V any] interface { - sort.Interface - Push(x V) // add x as element Len() - Pop() V // remove and return element Len() - 1. -} - -// Init establishes the heap invariants required by the other routines in this package. -// Init is idempotent with respect to the heap invariants -// and may be called whenever the heap invariants may have been invalidated. -// The complexity is O(n) where n = h.Len(). -func Init[V any](h Interface[V]) { - // heapify - n := h.Len() - for i := n/2 - 1; i >= 0; i-- { - down(h, i, n) - } -} - -// Push pushes the element x onto the heap. -// The complexity is O(log n) where n = h.Len(). -func Push[V any](h Interface[V], x V) { - h.Push(x) - up(h, h.Len()-1) -} - -// Pop removes and returns the minimum element (according to Less) from the heap. -// The complexity is O(log n) where n = h.Len(). -// Pop is equivalent to Remove(h, 0). -func Pop[V any](h Interface[V]) V { - n := h.Len() - 1 - h.Swap(0, n) - down(h, 0, n) - return h.Pop() -} - -// Remove removes and returns the element at index i from the heap. -// The complexity is O(log n) where n = h.Len(). -func Remove[V any](h Interface[V], i int) V { - n := h.Len() - 1 - if n != i { - h.Swap(i, n) - if !down(h, i, n) { - up(h, i) - } - } - return h.Pop() -} - -// Fix re-establishes the heap ordering after the element at index i has changed its value. -// Changing the value of the element at index i and then calling Fix is equivalent to, -// but less expensive than, calling Remove(h, i) followed by a Push of the new value. -// The complexity is O(log n) where n = h.Len(). -func Fix[V any](h Interface[V], i int) { - if !down(h, i, h.Len()) { - up(h, i) - } -} - -func up[V any](h Interface[V], j int) { - for { - i := (j - 1) / 2 // parent - if i == j || !h.Less(j, i) { - break - } - h.Swap(i, j) - j = i - } -} - -func down[V any](h Interface[V], i0, n int) bool { - i := i0 - for { - j1 := 2*i + 1 - if j1 >= n || j1 < 0 { // j1 < 0 after int overflow - break - } - j := j1 // left child - if j2 := j1 + 1; j2 < n && h.Less(j2, j1) { - j = j2 // = 2*i + 2 // right child - } - if !h.Less(j, i) { - break - } - h.Swap(i, j) - i = j - } - return i > i0 -} +// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package heap provides heap operations for any type that implements
+// heap.Interface. A heap is a tree with the property that each node is the
+// minimum-valued node in its subtree.
+//
+// The minimum element in the tree is the root, at index 0.
+//
+// A heap is a common way to implement a priority queue. To build a priority
+// queue, implement the Heap interface with the (negative) priority as the
+// ordering for the Less method, so Push adds items while Pop removes the
+// highest-priority item from the queue. The Examples include such an
+// implementation; the file example_pq_test.go has the complete source.
+//
+// This package is a copy of the Go standard library's
+// container/heap, but using generics.
+package heap
+
+import "sort"
+
+// The Interface type describes the requirements
+// for a type using the routines in this package.
+// Any type that implements it may be used as a
+// min-heap with the following invariants (established after
+// Init has been called or if the data is empty or sorted):
+//
+// !h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len()
+//
+// Note that Push and Pop in this interface are for package heap's
+// implementation to call. To add and remove things from the heap,
+// use heap.Push and heap.Pop.
+type Interface[V any] interface {
+ sort.Interface
+ Push(x V) // add x as element Len()
+ Pop() V // remove and return element Len() - 1.
+}
+
+// Init establishes the heap invariants required by the other routines in this package.
+// Init is idempotent with respect to the heap invariants
+// and may be called whenever the heap invariants may have been invalidated.
+// The complexity is O(n) where n = h.Len().
+func Init[V any](h Interface[V]) {
+ // heapify
+ n := h.Len()
+ for i := n/2 - 1; i >= 0; i-- {
+ down(h, i, n)
+ }
+}
+
+// Push pushes the element x onto the heap.
+// The complexity is O(log n) where n = h.Len().
+func Push[V any](h Interface[V], x V) {
+ h.Push(x)
+ up(h, h.Len()-1)
+}
+
+// Pop removes and returns the minimum element (according to Less) from the heap.
+// The complexity is O(log n) where n = h.Len().
+// Pop is equivalent to Remove(h, 0).
+func Pop[V any](h Interface[V]) V {
+ n := h.Len() - 1
+ h.Swap(0, n)
+ down(h, 0, n)
+ return h.Pop()
+}
+
+// Remove removes and returns the element at index i from the heap.
+// The complexity is O(log n) where n = h.Len().
+func Remove[V any](h Interface[V], i int) V {
+ n := h.Len() - 1
+ if n != i {
+ h.Swap(i, n)
+ if !down(h, i, n) {
+ up(h, i)
+ }
+ }
+ return h.Pop()
+}
+
+// Fix re-establishes the heap ordering after the element at index i has changed its value.
+// Changing the value of the element at index i and then calling Fix is equivalent to,
+// but less expensive than, calling Remove(h, i) followed by a Push of the new value.
+// The complexity is O(log n) where n = h.Len().
+func Fix[V any](h Interface[V], i int) {
+ if !down(h, i, h.Len()) {
+ up(h, i)
+ }
+}
+
+func up[V any](h Interface[V], j int) {
+ for {
+ i := (j - 1) / 2 // parent
+ if i == j || !h.Less(j, i) {
+ break
+ }
+ h.Swap(i, j)
+ j = i
+ }
+}
+
+func down[V any](h Interface[V], i0, n int) bool {
+ i := i0
+ for {
+ j1 := 2*i + 1
+ if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
+ break
+ }
+ j := j1 // left child
+ if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {
+ j = j2 // = 2*i + 2 // right child
+ }
+ if !h.Less(j, i) {
+ break
+ }
+ h.Swap(i, j)
+ i = j
+ }
+ return i > i0
+}
|
