Compare commits

..

2 Commits

Author SHA1 Message Date
Kaleb Elwert
4703ad4dc1 Expose the gossh.ServerConfig rather than specific values 2018-11-02 17:22:07 -07:00
Kaleb Elwert
cd0f9291c6 Directly expose the SSH server KEXT, MAC and Cipher algorithms 2018-11-02 11:42:13 -07:00
30 changed files with 158 additions and 686 deletions

2
.gitignore vendored

@ -1,2 +0,0 @@
.idea/
vendor/

@ -93,4 +93,4 @@ Become a sponsor and get your logo on our README on Github with a link to your s
## License
[BSD](LICENSE)
BSD

@ -6,17 +6,17 @@ import (
"io"
"log"
"git.tcp.direct/bfu/glider-ssh"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/gliderlabs/ssh"
)
func main() {
ssh.Handle(func(sess ssh.Session) {
_, _, isTty := sess.Pty()
var cfg = &container.Config{
cfg := &container.Config{
Image: sess.User(),
Cmd: sess.Command(),
Env: sess.Environ(),

@ -5,7 +5,7 @@ import (
"log"
"os/exec"
"git.tcp.direct/bfu/glider-ssh"
"github.com/gliderlabs/ssh"
)
func main() {

@ -9,8 +9,8 @@ import (
"syscall"
"unsafe"
"git.tcp.direct/bfu/glider-ssh"
"github.com/creack/pty"
"github.com/gliderlabs/ssh"
"github.com/kr/pty"
)
func setWinsize(f *os.File, w, h int) {
@ -37,7 +37,6 @@ func main() {
io.Copy(f, s) // stdin
}()
io.Copy(s, f) // stdout
cmd.Wait()
} else {
io.WriteString(s, "No PTY requested.\n")
s.Exit(1)

@ -5,7 +5,7 @@ import (
"io"
"log"
"git.tcp.direct/bfu/glider-ssh"
"github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh"
)

@ -1,37 +0,0 @@
package main
import (
"io"
"log"
"github.com/gliderlabs/ssh"
)
func main() {
log.Println("starting ssh server on port 2222...")
forwardHandler := &ssh.ForwardedTCPHandler{}
server := ssh.Server{
LocalPortForwardingCallback: ssh.LocalPortForwardingCallback(func(ctx ssh.Context, dhost string, dport uint32) bool {
log.Println("Accepted forward", dhost, dport)
return true
}),
Addr: ":2222",
Handler: ssh.Handler(func(s ssh.Session) {
io.WriteString(s, "Remote forwarding available...\n")
select {}
}),
ReversePortForwardingCallback: ssh.ReversePortForwardingCallback(func(ctx ssh.Context, host string, port uint32) bool {
log.Println("attempt to bind", host, port, "granted")
return true
}),
RequestHandlers: map[string]ssh.RequestHandler{
"tcpip-forward": forwardHandler.HandleSSHRequest,
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
},
}
log.Fatal(server.ListenAndServe())
}

@ -5,7 +5,7 @@ import (
"io"
"log"
"git.tcp.direct/bfu/glider-ssh"
"github.com/gliderlabs/ssh"
)
func main() {

@ -4,7 +4,7 @@ import (
"log"
"time"
"git.tcp.direct/bfu/glider-ssh"
"github.com/gliderlabs/ssh"
)
var (

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import (
"io"

26
circle.yml Normal file

@ -0,0 +1,26 @@
version: 2
jobs:
build-go-latest:
docker:
- image: golang:latest
working_directory: /go/src/github.com/gliderlabs/ssh
steps:
- checkout
- run: go get
- run: go test -v -race
build-go-1.9:
docker:
- image: golang:1.9
working_directory: /go/src/github.com/gliderlabs/ssh
steps:
- checkout
- run: go get
- run: go test -v -race
workflows:
version: 2
build:
jobs:
- build-go-latest
- build-go-1.9

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import (
"context"
@ -44,7 +44,7 @@ func (c *serverConn) updateDeadline() {
switch {
case c.idleTimeout > 0:
idleDeadline := time.Now().Add(c.idleTimeout)
if idleDeadline.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() {
if idleDeadline.Unix() < c.maxDeadline.Unix() {
c.Conn.SetDeadline(idleDeadline)
return
}

@ -1,10 +1,9 @@
package glider_ssh
package ssh
import (
"context"
"encoding/hex"
"net"
"sync"
gossh "golang.org/x/crypto/ssh"
)
@ -49,7 +48,7 @@ var (
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.
// The associated value will be of type gossh.Conn.
ContextKeyConn = &contextKey{"ssh-conn"}
// ContextKeyPublicKey is a context key for use with Contexts in this package.
@ -60,11 +59,9 @@ var (
// 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.
// exposed on Session in the session Handler.
type Context interface {
context.Context
sync.Locker
// User returns the username used when establishing the SSH connection.
User() string
@ -93,12 +90,11 @@ type Context 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 := &sshContext{innerCtx}
ctx.SetValue(ContextKeyServer, srv)
perms := &Permissions{&gossh.Permissions{}}
ctx.SetValue(ContextKeyPermissions, perms)
@ -140,10 +136,7 @@ func (ctx *sshContext) ServerVersion() string {
}
func (ctx *sshContext) RemoteAddr() net.Addr {
if addr, ok := ctx.Value(ContextKeyRemoteAddr).(net.Addr); ok {
return addr
}
return nil
return ctx.Value(ContextKeyRemoteAddr).(net.Addr)
}
func (ctx *sshContext) LocalAddr() net.Addr {

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import "testing"

4
doc.go

@ -1,4 +1,5 @@
/*
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.
@ -41,5 +42,6 @@ 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 glider_ssh
package ssh

@ -1,10 +1,10 @@
package glider_ssh_test
package ssh_test
import (
"io"
"io/ioutil"
"git.tcp.direct/bfu/glider-ssh"
"github.com/gliderlabs/ssh"
)
func ExampleListenAndServe() {

8
go.mod

@ -1,8 +0,0 @@
module git.tcp.direct/bfu/glider-ssh
go 1.13
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
)

13
go.sum

@ -1,13 +0,0 @@
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import (
"io/ioutil"
@ -42,13 +42,6 @@ func HostKeyFile(filepath string) Option {
}
}
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 {

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import (
"net"
@ -95,7 +95,7 @@ func TestConnWrapping(t *testing.T) {
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
}, PasswordAuth(func(ctx Context, password string) bool {
return true
}), WrapConn(func(ctx Context, conn net.Conn) net.Conn {
}), WrapConn(func(conn net.Conn) net.Conn {
wrapped = &wrappedConn{conn, 0}
return wrapped
}))

181
server.go

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import (
"context"
@ -15,20 +15,6 @@ import (
// 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.
@ -38,47 +24,35 @@ type Server struct {
HostSigners []Signer // private keys for the host key, must have at least one
Version string // server version to be sent before the initial handshake
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil
ServerConfigCallback ServerConfigCallback // callback for configuring detailed SSH options
SessionRequestCallback SessionRequestCallback // callback for allowing or denying SSH sessions
ConnectionFailedCallback ConnectionFailedCallback // callback to report connection failures
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
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
// Internal x/crypto/ssh config. Note that a number of values in this struct
// are overwritten every time a connection starts, so only use this if you
// know what you're doing and absolutely need to change the internal config
// values.
BaseConfig *gossh.ServerConfig
// 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
channelHandlers map[string]channelHandler
listenerWg sync.WaitGroup
mu sync.RWMutex
mu sync.Mutex
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()
// internal for now
type channelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context)
func (srv *Server) ensureHostSigner() error {
if len(srv.HostSigners) == 0 {
signer, err := generateSigner()
if err != nil {
@ -89,44 +63,18 @@ func (srv *Server) ensureHostSigner() error {
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 {
// Use the provided base config if set, otherwise default to an empty
// config.
config := srv.BaseConfig
if config == 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 {
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil {
config.NoClientAuth = true
}
if srv.Version != "" {
@ -151,23 +99,12 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
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
}
}
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
}
@ -179,7 +116,6 @@ func (srv *Server) Handle(fn Handler) {
func (srv *Server) Close() error {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.closeDoneChanLocked()
err := srv.closeListenersLocked()
for c := range srv.conns {
@ -221,7 +157,6 @@ func (srv *Server) Shutdown(ctx context.Context) error {
//
// 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
@ -229,6 +164,12 @@ func (srv *Server) Serve(l net.Listener) error {
if srv.Handler == nil {
srv.Handler = DefaultHandler
}
if srv.channelHandlers == nil {
srv.channelHandlers = map[string]channelHandler{
"session": sessionHandler,
"direct-tcpip": directTcpipHandler,
}
}
var tempDelay time.Duration
srv.trackListener(l, true)
@ -255,20 +196,20 @@ func (srv *Server) Serve(l net.Listener) error {
}
return e
}
go srv.HandleConn(conn)
go srv.handleConn(conn)
}
}
func (srv *Server) HandleConn(newConn net.Conn) {
ctx, cancel := newContext(srv)
func (srv *Server) handleConn(newConn net.Conn) {
if srv.ConnCallback != nil {
cbConn := srv.ConnCallback(ctx, newConn)
cbConn := srv.ConnCallback(newConn)
if cbConn == nil {
newConn.Close()
return
}
newConn = cbConn
}
ctx, cancel := newContext(srv)
conn := &serverConn{
Conn: newConn,
idleTimeout: srv.IdleTimeout,
@ -280,9 +221,7 @@ func (srv *Server) HandleConn(newConn net.Conn) {
defer conn.Close()
sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx))
if err != nil {
if srv.ConnectionFailedCallback != nil {
srv.ConnectionFailedCallback(conn, err)
}
// TODO: trigger event callback
return
}
@ -291,14 +230,10 @@ func (srv *Server) HandleConn(newConn net.Conn) {
ctx.SetValue(ContextKeyConn, sshConn)
applyConnMetadata(ctx, sshConn)
//go gossh.DiscardRequests(reqs)
go srv.handleRequests(ctx, reqs)
go gossh.DiscardRequests(reqs)
for ch := range chans {
handler := srv.ChannelHandlers[ch.ChannelType()]
if handler == nil {
handler = srv.ChannelHandlers["default"]
}
if handler == nil {
handler, found := srv.channelHandlers[ch.ChannelType()]
if !found {
ch.Reject(gossh.UnknownChannelType, "unsupported channel type")
continue
}
@ -306,23 +241,6 @@ func (srv *Server) HandleConn(newConn net.Conn) {
}
}
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.
@ -342,42 +260,19 @@ func (srv *Server) ListenAndServe() error {
// 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()
}
@ -414,7 +309,6 @@ func (srv *Server) closeListenersLocked() error {
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{})
}
@ -435,7 +329,6 @@ func (srv *Server) trackListener(ln net.Listener, add bool) {
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{})
}

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import (
"bytes"
@ -8,26 +8,6 @@ import (
"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")

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import (
"bytes"
@ -14,7 +14,7 @@ import (
// 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.
// cypto/ssh.
//
// When Command() returns an empty slice, the user requested a shell. Otherwise
// the user is performing an exec with those command arguments.
@ -44,12 +44,6 @@ type Session interface {
// 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
@ -77,32 +71,24 @@ type Session interface {
// 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)
}
// 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) {
func sessionHandler(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,
Channel: ch,
conn: conn,
handler: srv.Handler,
ptyCb: srv.PtyCallback,
ctx: ctx,
}
sess.handleRequests(reqs)
}
@ -110,22 +96,18 @@ func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.Ne
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
conn *gossh.ServerConn
handler Handler
handled bool
exited bool
pty *Pty
winch chan Window
env []string
ptyCb PtyCallback
cmd []string
ctx Context
sigCh chan<- Signal
sigBuf []Signal
}
func (sess *session) Write(p []byte) (n int, err error) {
@ -195,17 +177,8 @@ 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
return append([]string(nil), sess.cmd...)
}
func (sess *session) Pty() (Pty, <-chan Window, bool) {
@ -228,12 +201,6 @@ func (sess *session) Signals(c chan<- Signal) {
}
}
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 {
@ -242,60 +209,16 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
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)
var payload = struct{ Value string }{}
gossh.Unmarshal(req.Payload, &payload)
sess.cmd, _ = shlex.Split(payload.Value, true)
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)
@ -357,18 +280,8 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
// 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,4 +1,4 @@
package glider_ssh
package ssh
import (
"bytes"
@ -11,7 +11,6 @@ import (
)
func (srv *Server) serveOnce(l net.Listener) error {
srv.ensureHandlers()
if err := srv.ensureHostSigner(); err != nil {
return err
}
@ -19,11 +18,11 @@ func (srv *Server) serveOnce(l net.Listener) error {
if e != nil {
return e
}
srv.ChannelHandlers = map[string]ChannelHandler{
"session": DefaultSessionHandler,
"direct-tcpip": DirectTCPIPHandler,
srv.channelHandlers = map[string]channelHandler{
"session": sessionHandler,
"direct-tcpip": directTcpipHandler,
}
srv.HandleConn(conn)
srv.handleConn(conn)
return nil
}
@ -289,40 +288,20 @@ func TestPtyResize(t *testing.T) {
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)
signals := make(chan Signal)
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
if sig := <-signals; sig != SIGINT {
t.Fatalf("expected signal %v but got %v", SIGINT, sig)
}
select {
case sig := <-signals:
if sig != SIGKILL {
errChan <- fmt.Errorf("expected signal %v but got %v", SIGKILL, sig)
return
exiter := make(chan bool)
go func() {
if sig := <-signals; sig == SIGKILL {
close(exiter)
}
case <-doneChan:
errChan <- fmt.Errorf("Unexpected done")
return
}
}()
<-exiter
},
}, nil)
defer cleanup()
@ -332,106 +311,7 @@ func TestSignals(t *testing.T) {
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)
err := session.Run("")
if err != nil {
t.Fatalf("expected nil but got %v", err)
}

34
ssh.go

@ -1,10 +1,8 @@
package glider_ssh
package ssh
import (
"crypto/subtle"
"net"
gossh "golang.org/x/crypto/ssh"
)
type Signal string
@ -41,33 +39,17 @@ type PublicKeyHandler func(ctx Context, key PublicKey) bool
// PasswordHandler is a callback for performing password authentication.
type PasswordHandler func(ctx Context, password string) bool
// KeyboardInteractiveHandler is a callback for performing keyboard-interactive authentication.
type KeyboardInteractiveHandler func(ctx Context, challenger gossh.KeyboardInteractiveChallenge) bool
// PtyCallback is a hook for allowing PTY sessions.
type PtyCallback func(ctx Context, pty Pty) bool
// SessionRequestCallback is a callback for allowing or denying SSH sessions.
type SessionRequestCallback func(sess Session, requestType string) bool
// ConnCallback is a hook for new connections before handling.
// It allows wrapping for timeouts and limiting by returning
// the net.Conn that will be used as the underlying connection.
type ConnCallback func(ctx Context, conn net.Conn) net.Conn
type ConnCallback func(conn net.Conn) net.Conn
// LocalPortForwardingCallback is a hook for allowing port forwarding
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
// ReversePortForwardingCallback is a hook for allowing reverse port forwarding
type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool
// ServerConfigCallback is a hook for creating custom default server configs
type ServerConfigCallback func(ctx Context) *gossh.ServerConfig
// 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.
type Window struct {
Width int
@ -85,27 +67,27 @@ type Pty struct {
// 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) (*Server, error) {
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 nil, err
return err
}
}
return srv, srv.Serve(l)
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) (*Server, error) {
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 nil, err
return err
}
}
return srv, srv.ListenAndServe()
return srv.ListenAndServe()
}
// Handle registers the handler as the DefaultHandler.

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import (
"testing"

157
tcpip.go

@ -1,44 +1,36 @@
package glider_ssh
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
type forwardData struct {
DestinationHost string
DestinationPort uint32
OriginAddr string
OriginPort uint32
OriginatorHost string
OriginatorPort 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{}
func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
d := forwardData{}
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) {
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestinationHost, d.DestinationPort) {
newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
return
}
dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10))
dest := net.JoinHostPort(d.DestinationHost, strconv.FormatInt(int64(d.DestinationPort), 10))
var dialer net.Dialer
dconn, err := dialer.DialContext(ctx, "tcp", dest)
if err != nil {
@ -64,130 +56,3 @@ func DirectTCPIPHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewCh
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,4 +1,4 @@
package glider_ssh
package ssh
import (
"bytes"

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import (
"crypto/rand"
@ -22,10 +22,16 @@ func parsePtyRequest(s []byte) (pty Pty, ok bool) {
return
}
width32, s, ok := parseUint32(s)
if width32 < 1 {
ok = false
}
if !ok {
return
}
height32, _, ok := parseUint32(s)
if height32 < 1 {
ok = false
}
if !ok {
return
}

@ -1,4 +1,4 @@
package glider_ssh
package ssh
import gossh "golang.org/x/crypto/ssh"