summaryrefslogtreecommitdiffhomepage
path: root/tempfork
diff options
context:
space:
mode:
Diffstat (limited to 'tempfork')
-rw-r--r--tempfork/gliderlabs/ssh/LICENSE54
-rw-r--r--tempfork/gliderlabs/ssh/README.md192
-rw-r--r--tempfork/gliderlabs/ssh/agent.go166
-rw-r--r--tempfork/gliderlabs/ssh/conn.go110
-rw-r--r--tempfork/gliderlabs/ssh/context.go328
-rw-r--r--tempfork/gliderlabs/ssh/context_test.go98
-rw-r--r--tempfork/gliderlabs/ssh/doc.go90
-rw-r--r--tempfork/gliderlabs/ssh/example_test.go100
-rw-r--r--tempfork/gliderlabs/ssh/options.go168
-rw-r--r--tempfork/gliderlabs/ssh/options_test.go222
-rw-r--r--tempfork/gliderlabs/ssh/server.go918
-rw-r--r--tempfork/gliderlabs/ssh/server_test.go256
-rw-r--r--tempfork/gliderlabs/ssh/session.go772
-rw-r--r--tempfork/gliderlabs/ssh/session_test.go880
-rw-r--r--tempfork/gliderlabs/ssh/ssh.go312
-rw-r--r--tempfork/gliderlabs/ssh/ssh_test.go34
-rw-r--r--tempfork/gliderlabs/ssh/tcpip.go386
-rw-r--r--tempfork/gliderlabs/ssh/tcpip_test.go170
-rw-r--r--tempfork/gliderlabs/ssh/util.go314
-rw-r--r--tempfork/gliderlabs/ssh/wrap.go66
-rw-r--r--tempfork/heap/heap.go242
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
-
-[![GoDoc](https://godoc.org/tailscale.com/tempfork/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh)
-[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh)
-[![Go Report Card](https://goreportcard.com/badge/tailscale.com/tempfork/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh)
-[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors)
-[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com)
-[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312)
-
-> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member
-
-This Go package wraps the [crypto/ssh
-package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
-building SSH servers. The goal of the API was to make it as simple as using
-[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
-
-```go
- package main
-
- import (
- "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
+
+[![GoDoc](https://godoc.org/tailscale.com/tempfork/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh)
+[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh)
+[![Go Report Card](https://goreportcard.com/badge/tailscale.com/tempfork/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh)
+[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors)
+[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com)
+[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312)
+
+> The Glider Labs SSH server package is dope. &mdash;[@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
+}