Updates #12409 Updates #5295 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>main
parent
21695cdbf8
commit
82fa218c4a
@ -1,27 +0,0 @@ |
|||||||
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. |
|
||||||
@ -1,96 +0,0 @@ |
|||||||
# gliderlabs/ssh |
|
||||||
|
|
||||||
[](https://godoc.org/github.com/gliderlabs/ssh) |
|
||||||
[](https://circleci.com/gh/gliderlabs/ssh) |
|
||||||
[](https://goreportcard.com/report/github.com/gliderlabs/ssh) |
|
||||||
[](#sponsors) |
|
||||||
[](http://slack.gliderlabs.com) |
|
||||||
[](https://app.convertkit.com/landing_pages/243312) |
|
||||||
|
|
||||||
> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member |
|
||||||
|
|
||||||
This Go package wraps the [crypto/ssh |
|
||||||
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for |
|
||||||
building SSH servers. The goal of the API was to make it as simple as using |
|
||||||
[net/http](https://golang.org/pkg/net/http/), so the API is very similar: |
|
||||||
|
|
||||||
```go |
|
||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"tailscale.com/tempfork/gliderlabs/ssh" |
|
||||||
"io" |
|
||||||
"log" |
|
||||||
) |
|
||||||
|
|
||||||
func main() { |
|
||||||
ssh.Handle(func(s ssh.Session) { |
|
||||||
io.WriteString(s, "Hello world\n") |
|
||||||
}) |
|
||||||
|
|
||||||
log.Fatal(ssh.ListenAndServe(":2222", nil)) |
|
||||||
} |
|
||||||
|
|
||||||
``` |
|
||||||
This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)). |
|
||||||
|
|
||||||
## Examples |
|
||||||
|
|
||||||
A bunch of great examples are in the `_examples` directory. |
|
||||||
|
|
||||||
## Usage |
|
||||||
|
|
||||||
[See GoDoc reference.](https://godoc.org/tailscale.com/tempfork/gliderlabs/ssh) |
|
||||||
|
|
||||||
## Contributing |
|
||||||
|
|
||||||
Pull requests are welcome! However, since this project is very much about API |
|
||||||
design, please submit API changes as issues to discuss before submitting PRs. |
|
||||||
|
|
||||||
Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well. |
|
||||||
|
|
||||||
## Roadmap |
|
||||||
|
|
||||||
* Non-session channel handlers |
|
||||||
* Cleanup callback API |
|
||||||
* 1.0 release |
|
||||||
* High-level client? |
|
||||||
|
|
||||||
## Sponsors |
|
||||||
|
|
||||||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)] |
|
||||||
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a> |
|
||||||
<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a> |
|
||||||
|
|
||||||
## License |
|
||||||
|
|
||||||
[BSD](LICENSE) |
|
||||||
@ -1,83 +0,0 @@ |
|||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"io" |
|
||||||
"net" |
|
||||||
"os" |
|
||||||
"path" |
|
||||||
"sync" |
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
agentRequestType = "auth-agent-req@openssh.com" |
|
||||||
agentChannelType = "auth-agent@openssh.com" |
|
||||||
|
|
||||||
agentTempDir = "auth-agent" |
|
||||||
agentListenFile = "listener.sock" |
|
||||||
) |
|
||||||
|
|
||||||
// contextKeyAgentRequest is an internal context key for storing if the
|
|
||||||
// client requested agent forwarding
|
|
||||||
var contextKeyAgentRequest = &contextKey{"auth-agent-req"} |
|
||||||
|
|
||||||
// SetAgentRequested sets up the session context so that AgentRequested
|
|
||||||
// returns true.
|
|
||||||
func SetAgentRequested(ctx Context) { |
|
||||||
ctx.SetValue(contextKeyAgentRequest, true) |
|
||||||
} |
|
||||||
|
|
||||||
// AgentRequested returns true if the client requested agent forwarding.
|
|
||||||
func AgentRequested(sess Session) bool { |
|
||||||
return sess.Context().Value(contextKeyAgentRequest) == true |
|
||||||
} |
|
||||||
|
|
||||||
// NewAgentListener sets up a temporary Unix socket that can be communicated
|
|
||||||
// to the session environment and used for forwarding connections.
|
|
||||||
func NewAgentListener() (net.Listener, error) { |
|
||||||
dir, err := 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) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,55 +0,0 @@ |
|||||||
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) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,155 +0,0 @@ |
|||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"encoding/hex" |
|
||||||
"net" |
|
||||||
"sync" |
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh" |
|
||||||
) |
|
||||||
|
|
||||||
// contextKey is a value for use with context.WithValue. It's used as
|
|
||||||
// a pointer so it fits in an interface{} without allocation.
|
|
||||||
type contextKey struct { |
|
||||||
name string |
|
||||||
} |
|
||||||
|
|
||||||
var ( |
|
||||||
// ContextKeyUser is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type string.
|
|
||||||
ContextKeyUser = &contextKey{"user"} |
|
||||||
|
|
||||||
// ContextKeySessionID is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type string.
|
|
||||||
ContextKeySessionID = &contextKey{"session-id"} |
|
||||||
|
|
||||||
// ContextKeyPermissions is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type *Permissions.
|
|
||||||
ContextKeyPermissions = &contextKey{"permissions"} |
|
||||||
|
|
||||||
// ContextKeyClientVersion is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type string.
|
|
||||||
ContextKeyClientVersion = &contextKey{"client-version"} |
|
||||||
|
|
||||||
// ContextKeyServerVersion is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type string.
|
|
||||||
ContextKeyServerVersion = &contextKey{"server-version"} |
|
||||||
|
|
||||||
// ContextKeyLocalAddr is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type net.Addr.
|
|
||||||
ContextKeyLocalAddr = &contextKey{"local-addr"} |
|
||||||
|
|
||||||
// ContextKeyRemoteAddr is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type net.Addr.
|
|
||||||
ContextKeyRemoteAddr = &contextKey{"remote-addr"} |
|
||||||
|
|
||||||
// ContextKeyServer is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type *Server.
|
|
||||||
ContextKeyServer = &contextKey{"ssh-server"} |
|
||||||
|
|
||||||
// ContextKeyConn is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type gossh.ServerConn.
|
|
||||||
ContextKeyConn = &contextKey{"ssh-conn"} |
|
||||||
|
|
||||||
// ContextKeyPublicKey is a context key for use with Contexts in this package.
|
|
||||||
// The associated value will be of type PublicKey.
|
|
||||||
ContextKeyPublicKey = &contextKey{"public-key"} |
|
||||||
) |
|
||||||
|
|
||||||
// Context is a package specific context interface. It exposes connection
|
|
||||||
// metadata and allows new values to be easily written to it. It's used in
|
|
||||||
// authentication handlers and callbacks, and its underlying context.Context is
|
|
||||||
// exposed on Session in the session Handler. A connection-scoped lock is also
|
|
||||||
// embedded in the context to make it easier to limit operations per-connection.
|
|
||||||
type Context interface { |
|
||||||
context.Context |
|
||||||
sync.Locker |
|
||||||
|
|
||||||
// User returns the username used when establishing the SSH connection.
|
|
||||||
User() string |
|
||||||
|
|
||||||
// SessionID returns the session hash.
|
|
||||||
SessionID() string |
|
||||||
|
|
||||||
// ClientVersion returns the version reported by the client.
|
|
||||||
ClientVersion() string |
|
||||||
|
|
||||||
// ServerVersion returns the version reported by the server.
|
|
||||||
ServerVersion() string |
|
||||||
|
|
||||||
// RemoteAddr returns the remote address for this connection.
|
|
||||||
RemoteAddr() net.Addr |
|
||||||
|
|
||||||
// LocalAddr returns the local address for this connection.
|
|
||||||
LocalAddr() net.Addr |
|
||||||
|
|
||||||
// Permissions returns the Permissions object used for this connection.
|
|
||||||
Permissions() *Permissions |
|
||||||
|
|
||||||
// SetValue allows you to easily write new values into the underlying context.
|
|
||||||
SetValue(key, value interface{}) |
|
||||||
} |
|
||||||
|
|
||||||
type sshContext struct { |
|
||||||
context.Context |
|
||||||
*sync.Mutex |
|
||||||
} |
|
||||||
|
|
||||||
func newContext(srv *Server) (*sshContext, context.CancelFunc) { |
|
||||||
innerCtx, cancel := context.WithCancel(context.Background()) |
|
||||||
ctx := &sshContext{innerCtx, &sync.Mutex{}} |
|
||||||
ctx.SetValue(ContextKeyServer, srv) |
|
||||||
perms := &Permissions{&gossh.Permissions{}} |
|
||||||
ctx.SetValue(ContextKeyPermissions, perms) |
|
||||||
return ctx, cancel |
|
||||||
} |
|
||||||
|
|
||||||
// this is separate from newContext because we will get ConnMetadata
|
|
||||||
// at different points so it needs to be applied separately
|
|
||||||
func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) { |
|
||||||
if ctx.Value(ContextKeySessionID) != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
ctx.SetValue(ContextKeySessionID, hex.EncodeToString(conn.SessionID())) |
|
||||||
ctx.SetValue(ContextKeyClientVersion, string(conn.ClientVersion())) |
|
||||||
ctx.SetValue(ContextKeyServerVersion, string(conn.ServerVersion())) |
|
||||||
ctx.SetValue(ContextKeyUser, conn.User()) |
|
||||||
ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr()) |
|
||||||
ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr()) |
|
||||||
} |
|
||||||
|
|
||||||
func (ctx *sshContext) SetValue(key, value interface{}) { |
|
||||||
ctx.Context = context.WithValue(ctx.Context, key, value) |
|
||||||
} |
|
||||||
|
|
||||||
func (ctx *sshContext) User() string { |
|
||||||
return ctx.Value(ContextKeyUser).(string) |
|
||||||
} |
|
||||||
|
|
||||||
func (ctx *sshContext) SessionID() string { |
|
||||||
return ctx.Value(ContextKeySessionID).(string) |
|
||||||
} |
|
||||||
|
|
||||||
func (ctx *sshContext) ClientVersion() string { |
|
||||||
return ctx.Value(ContextKeyClientVersion).(string) |
|
||||||
} |
|
||||||
|
|
||||||
func (ctx *sshContext) ServerVersion() string { |
|
||||||
return ctx.Value(ContextKeyServerVersion).(string) |
|
||||||
} |
|
||||||
|
|
||||||
func (ctx *sshContext) RemoteAddr() net.Addr { |
|
||||||
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) |
|
||||||
} |
|
||||||
@ -1,49 +0,0 @@ |
|||||||
//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) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,45 +0,0 @@ |
|||||||
/* |
|
||||||
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 |
|
||||||
@ -1,50 +0,0 @@ |
|||||||
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")) |
|
||||||
} |
|
||||||
@ -1,84 +0,0 @@ |
|||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"os" |
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh" |
|
||||||
) |
|
||||||
|
|
||||||
// PasswordAuth returns a functional option that sets PasswordHandler on the server.
|
|
||||||
func PasswordAuth(fn PasswordHandler) Option { |
|
||||||
return func(srv *Server) error { |
|
||||||
srv.PasswordHandler = fn |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// PublicKeyAuth returns a functional option that sets PublicKeyHandler on the server.
|
|
||||||
func PublicKeyAuth(fn PublicKeyHandler) Option { |
|
||||||
return func(srv *Server) error { |
|
||||||
srv.PublicKeyHandler = fn |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// HostKeyFile returns a functional option that adds HostSigners to the server
|
|
||||||
// from a PEM file at filepath.
|
|
||||||
func HostKeyFile(filepath string) Option { |
|
||||||
return func(srv *Server) error { |
|
||||||
pemBytes, err := 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 |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,111 +0,0 @@ |
|||||||
//go:build glidertests
|
|
||||||
|
|
||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"net" |
|
||||||
"strings" |
|
||||||
"sync/atomic" |
|
||||||
"testing" |
|
||||||
|
|
||||||
gossh "golang.org/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") |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,459 +0,0 @@ |
|||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"net" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh" |
|
||||||
) |
|
||||||
|
|
||||||
// ErrServerClosed is returned by the Server's Serve, ListenAndServe,
|
|
||||||
// and ListenAndServeTLS methods after a call to Shutdown or Close.
|
|
||||||
var ErrServerClosed = errors.New("ssh: Server closed") |
|
||||||
|
|
||||||
type 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() |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,128 +0,0 @@ |
|||||||
//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 |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,386 +0,0 @@ |
|||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"net" |
|
||||||
"sync" |
|
||||||
|
|
||||||
"github.com/anmitsu/go-shlex" |
|
||||||
gossh "golang.org/x/crypto/ssh" |
|
||||||
) |
|
||||||
|
|
||||||
// Session provides access to information about an SSH session and methods
|
|
||||||
// to read and write to the SSH channel with an embedded Channel interface from
|
|
||||||
// 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) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,440 +0,0 @@ |
|||||||
//go:build glidertests
|
|
||||||
|
|
||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"net" |
|
||||||
"testing" |
|
||||||
|
|
||||||
gossh "golang.org/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) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,156 +0,0 @@ |
|||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/subtle" |
|
||||||
"net" |
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh" |
|
||||||
) |
|
||||||
|
|
||||||
type Signal string |
|
||||||
|
|
||||||
// POSIX signals as listed in RFC 4254 Section 6.10.
|
|
||||||
const ( |
|
||||||
SIGABRT Signal = "ABRT" |
|
||||||
SIGALRM Signal = "ALRM" |
|
||||||
SIGFPE Signal = "FPE" |
|
||||||
SIGHUP Signal = "HUP" |
|
||||||
SIGILL Signal = "ILL" |
|
||||||
SIGINT Signal = "INT" |
|
||||||
SIGKILL Signal = "KILL" |
|
||||||
SIGPIPE Signal = "PIPE" |
|
||||||
SIGQUIT Signal = "QUIT" |
|
||||||
SIGSEGV Signal = "SEGV" |
|
||||||
SIGTERM Signal = "TERM" |
|
||||||
SIGUSR1 Signal = "USR1" |
|
||||||
SIGUSR2 Signal = "USR2" |
|
||||||
) |
|
||||||
|
|
||||||
// DefaultHandler is the default Handler used by Serve.
|
|
||||||
var DefaultHandler Handler |
|
||||||
|
|
||||||
// Option is a functional option handler for Server.
|
|
||||||
type Option func(*Server) error |
|
||||||
|
|
||||||
// Handler is a callback for handling established SSH sessions.
|
|
||||||
type Handler func(Session) |
|
||||||
|
|
||||||
// PublicKeyHandler is a callback for performing public key authentication.
|
|
||||||
type PublicKeyHandler func(ctx Context, key PublicKey) 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 golang.org/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) |
|
||||||
} |
|
||||||
@ -1,17 +0,0 @@ |
|||||||
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") |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,193 +0,0 @@ |
|||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"io" |
|
||||||
"log" |
|
||||||
"net" |
|
||||||
"strconv" |
|
||||||
"sync" |
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
forwardedTCPChannelType = "forwarded-tcpip" |
|
||||||
) |
|
||||||
|
|
||||||
// direct-tcpip data struct as specified in RFC4254, Section 7.2
|
|
||||||
type localForwardChannelData struct { |
|
||||||
DestAddr string |
|
||||||
DestPort uint32 |
|
||||||
|
|
||||||
OriginAddr string |
|
||||||
OriginPort uint32 |
|
||||||
} |
|
||||||
|
|
||||||
// DirectTCPIPHandler can be enabled by adding it to the server's
|
|
||||||
// ChannelHandlers under direct-tcpip.
|
|
||||||
func DirectTCPIPHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) { |
|
||||||
d := localForwardChannelData{} |
|
||||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil { |
|
||||||
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error()) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestAddr, d.DestPort) { |
|
||||||
newChan.Reject(gossh.Prohibited, "port forwarding is disabled") |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10)) |
|
||||||
|
|
||||||
var dialer net.Dialer |
|
||||||
dconn, err := dialer.DialContext(ctx, "tcp", dest) |
|
||||||
if err != nil { |
|
||||||
newChan.Reject(gossh.ConnectionFailed, err.Error()) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
ch, reqs, err := newChan.Accept() |
|
||||||
if err != nil { |
|
||||||
dconn.Close() |
|
||||||
return |
|
||||||
} |
|
||||||
go gossh.DiscardRequests(reqs) |
|
||||||
|
|
||||||
go func() { |
|
||||||
defer ch.Close() |
|
||||||
defer dconn.Close() |
|
||||||
io.Copy(ch, dconn) |
|
||||||
}() |
|
||||||
go func() { |
|
||||||
defer ch.Close() |
|
||||||
defer dconn.Close() |
|
||||||
io.Copy(dconn, ch) |
|
||||||
}() |
|
||||||
} |
|
||||||
|
|
||||||
type remoteForwardRequest struct { |
|
||||||
BindAddr string |
|
||||||
BindPort uint32 |
|
||||||
} |
|
||||||
|
|
||||||
type remoteForwardSuccess struct { |
|
||||||
BindPort uint32 |
|
||||||
} |
|
||||||
|
|
||||||
type remoteForwardCancelRequest struct { |
|
||||||
BindAddr string |
|
||||||
BindPort uint32 |
|
||||||
} |
|
||||||
|
|
||||||
type remoteForwardChannelData struct { |
|
||||||
DestAddr string |
|
||||||
DestPort uint32 |
|
||||||
OriginAddr string |
|
||||||
OriginPort uint32 |
|
||||||
} |
|
||||||
|
|
||||||
// ForwardedTCPHandler can be enabled by creating a ForwardedTCPHandler and
|
|
||||||
// adding the HandleSSHRequest callback to the server's RequestHandlers under
|
|
||||||
// tcpip-forward and cancel-tcpip-forward.
|
|
||||||
type ForwardedTCPHandler struct { |
|
||||||
forwards map[string]net.Listener |
|
||||||
sync.Mutex |
|
||||||
} |
|
||||||
|
|
||||||
func (h *ForwardedTCPHandler) HandleSSHRequest(ctx Context, srv *Server, req *gossh.Request) (bool, []byte) { |
|
||||||
h.Lock() |
|
||||||
if h.forwards == nil { |
|
||||||
h.forwards = make(map[string]net.Listener) |
|
||||||
} |
|
||||||
h.Unlock() |
|
||||||
conn := ctx.Value(ContextKeyConn).(*gossh.ServerConn) |
|
||||||
switch req.Type { |
|
||||||
case "tcpip-forward": |
|
||||||
var reqPayload remoteForwardRequest |
|
||||||
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil { |
|
||||||
// TODO: log parse failure
|
|
||||||
return false, []byte{} |
|
||||||
} |
|
||||||
if srv.ReversePortForwardingCallback == nil || !srv.ReversePortForwardingCallback(ctx, reqPayload.BindAddr, reqPayload.BindPort) { |
|
||||||
return false, []byte("port forwarding is disabled") |
|
||||||
} |
|
||||||
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort))) |
|
||||||
ln, err := net.Listen("tcp", addr) |
|
||||||
if err != nil { |
|
||||||
// TODO: log listen failure
|
|
||||||
return false, []byte{} |
|
||||||
} |
|
||||||
_, destPortStr, _ := net.SplitHostPort(ln.Addr().String()) |
|
||||||
destPort, _ := strconv.Atoi(destPortStr) |
|
||||||
h.Lock() |
|
||||||
h.forwards[addr] = ln |
|
||||||
h.Unlock() |
|
||||||
go func() { |
|
||||||
<-ctx.Done() |
|
||||||
h.Lock() |
|
||||||
ln, ok := h.forwards[addr] |
|
||||||
h.Unlock() |
|
||||||
if ok { |
|
||||||
ln.Close() |
|
||||||
} |
|
||||||
}() |
|
||||||
go func() { |
|
||||||
for { |
|
||||||
c, err := ln.Accept() |
|
||||||
if err != nil { |
|
||||||
// TODO: log accept failure
|
|
||||||
break |
|
||||||
} |
|
||||||
originAddr, orignPortStr, _ := net.SplitHostPort(c.RemoteAddr().String()) |
|
||||||
originPort, _ := strconv.Atoi(orignPortStr) |
|
||||||
payload := gossh.Marshal(&remoteForwardChannelData{ |
|
||||||
DestAddr: reqPayload.BindAddr, |
|
||||||
DestPort: uint32(destPort), |
|
||||||
OriginAddr: originAddr, |
|
||||||
OriginPort: uint32(originPort), |
|
||||||
}) |
|
||||||
go func() { |
|
||||||
ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload) |
|
||||||
if err != nil { |
|
||||||
// TODO: log failure to open channel
|
|
||||||
log.Println(err) |
|
||||||
c.Close() |
|
||||||
return |
|
||||||
} |
|
||||||
go gossh.DiscardRequests(reqs) |
|
||||||
go func() { |
|
||||||
defer ch.Close() |
|
||||||
defer c.Close() |
|
||||||
io.Copy(ch, c) |
|
||||||
}() |
|
||||||
go func() { |
|
||||||
defer ch.Close() |
|
||||||
defer c.Close() |
|
||||||
io.Copy(c, ch) |
|
||||||
}() |
|
||||||
}() |
|
||||||
} |
|
||||||
h.Lock() |
|
||||||
delete(h.forwards, addr) |
|
||||||
h.Unlock() |
|
||||||
}() |
|
||||||
return true, gossh.Marshal(&remoteForwardSuccess{uint32(destPort)}) |
|
||||||
|
|
||||||
case "cancel-tcpip-forward": |
|
||||||
var reqPayload remoteForwardCancelRequest |
|
||||||
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil { |
|
||||||
// TODO: log parse failure
|
|
||||||
return false, []byte{} |
|
||||||
} |
|
||||||
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort))) |
|
||||||
h.Lock() |
|
||||||
ln, ok := h.forwards[addr] |
|
||||||
h.Unlock() |
|
||||||
if ok { |
|
||||||
ln.Close() |
|
||||||
} |
|
||||||
return true, nil |
|
||||||
default: |
|
||||||
return false, nil |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,85 +0,0 @@ |
|||||||
//go:build glidertests
|
|
||||||
|
|
||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"io" |
|
||||||
"net" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
"testing" |
|
||||||
|
|
||||||
gossh "golang.org/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) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,157 +0,0 @@ |
|||||||
package ssh |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/rand" |
|
||||||
"crypto/rsa" |
|
||||||
"encoding/binary" |
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh" |
|
||||||
) |
|
||||||
|
|
||||||
func generateSigner() (ssh.Signer, error) { |
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return ssh.NewSignerFromKey(key) |
|
||||||
} |
|
||||||
|
|
||||||
func parsePtyRequest(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 |
|
||||||
} |
|
||||||
@ -1,33 +0,0 @@ |
|||||||
package ssh |
|
||||||
|
|
||||||
import gossh "golang.org/x/crypto/ssh" |
|
||||||
|
|
||||||
// PublicKey is an abstraction of different types of public keys.
|
|
||||||
type PublicKey interface { |
|
||||||
gossh.PublicKey |
|
||||||
} |
|
||||||
|
|
||||||
// The Permissions type holds fine-grained permissions that are specific to a
|
|
||||||
// user or a specific authentication method for a user. Permissions, except for
|
|
||||||
// "source-address", must be enforced in the server application layer, after
|
|
||||||
// successful authentication.
|
|
||||||
type Permissions struct { |
|
||||||
*gossh.Permissions |
|
||||||
} |
|
||||||
|
|
||||||
// A Signer can create signatures that verify against a public key.
|
|
||||||
type Signer interface { |
|
||||||
gossh.Signer |
|
||||||
} |
|
||||||
|
|
||||||
// ParseAuthorizedKey parses a public key from an authorized_keys file used in
|
|
||||||
// OpenSSH according to the sshd(8) manual page.
|
|
||||||
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) { |
|
||||||
return gossh.ParseAuthorizedKey(in) |
|
||||||
} |
|
||||||
|
|
||||||
// ParsePublicKey parses an SSH public key formatted for use in
|
|
||||||
// the SSH wire protocol according to RFC 4253, section 6.6.
|
|
||||||
func ParsePublicKey(in []byte) (out PublicKey, err error) { |
|
||||||
return gossh.ParsePublicKey(in) |
|
||||||
} |
|
||||||
Loading…
Reference in new issue