Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: gliderlabs/ssh
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.1.1
Choose a base ref
...
head repository: gliderlabs/ssh
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Loading
Showing with 841 additions and 124 deletions.
  1. +1 −1 README.md
  2. +2 −1 _examples/ssh-pty/pty.go
  3. +37 −0 _examples/ssh-remoteforward/portforward.go
  4. +42 −0 _examples/ssh-sftpserver/sftp.go
  5. +2 −2 agent.go
  6. +3 −3 circle.yml
  7. +21 −13 conn.go
  8. +26 −5 context.go
  9. +39 −1 context_test.go
  10. +0 −2 doc.go
  11. +2 −2 example_test.go
  12. +10 −0 go.mod
  13. +7 −0 go.sum
  14. +9 −2 options.go
  15. +1 −1 options_test.go
  16. +168 −29 server.go
  17. +54 −0 server_test.go
  18. +111 −25 session.go
  19. +134 −14 session_test.go
  20. +23 −3 ssh.go
  21. +145 −10 tcpip.go
  22. +4 −4 tcpip_test.go
  23. +0 −6 util.go
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -93,4 +93,4 @@ Become a sponsor and get your logo on our README on Github with a link to your s

## License

BSD
[BSD](LICENSE)
3 changes: 2 additions & 1 deletion _examples/ssh-pty/pty.go
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import (
"unsafe"

"github.com/gliderlabs/ssh"
"github.com/kr/pty"
"github.com/creack/pty"
)

func setWinsize(f *os.File, w, h int) {
@@ -37,6 +37,7 @@ 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)
37 changes: 37 additions & 0 deletions _examples/ssh-remoteforward/portforward.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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())
}
42 changes: 42 additions & 0 deletions _examples/ssh-sftpserver/sftp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"fmt"
"io"
"log"

"github.com/gliderlabs/ssh"
"github.com/pkg/sftp"
)

// SftpHandler handler for SFTP subsystem
func SftpHandler(sess ssh.Session) {
debugStream := io.Discard
serverOptions := []sftp.ServerOption{
sftp.WithDebug(debugStream),
}
server, err := sftp.NewServer(
sess,
serverOptions...,
)
if err != nil {
log.Printf("sftp server init error: %s\n", err)
return
}
if err := server.Serve(); err == io.EOF {
server.Close()
fmt.Println("sftp client exited session.")
} else if err != nil {
fmt.Println("sftp server completed with error:", err)
}
}

func main() {
ssh_server := ssh.Server{
Addr: "127.0.0.1:2222",
SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": SftpHandler,
},
}
log.Fatal(ssh_server.ListenAndServe())
}
4 changes: 2 additions & 2 deletions agent.go
Original file line number Diff line number Diff line change
@@ -2,8 +2,8 @@ package ssh

import (
"io"
"io/ioutil"
"net"
"os"
"path"
"sync"

@@ -36,7 +36,7 @@ func AgentRequested(sess Session) bool {
// NewAgentListener sets up a temporary Unix socket that can be communicated
// to the session environment and used for forwarding connections.
func NewAgentListener() (net.Listener, error) {
dir, err := ioutil.TempDir("", agentTempDir)
dir, err := os.MkdirTemp("", agentTempDir)
if err != nil {
return nil, err
}
6 changes: 3 additions & 3 deletions circle.yml
Original file line number Diff line number Diff line change
@@ -9,9 +9,9 @@ jobs:
- run: go get
- run: go test -v -race

build-go-1.9:
build-go-1.20:
docker:
- image: golang:1.9
- image: golang:1.20
working_directory: /go/src/github.com/gliderlabs/ssh
steps:
- checkout
@@ -23,4 +23,4 @@ workflows:
build:
jobs:
- build-go-latest
- build-go-1.9
- build-go-1.20
34 changes: 21 additions & 13 deletions conn.go
Original file line number Diff line number Diff line change
@@ -9,13 +9,16 @@ import (
type serverConn struct {
net.Conn

idleTimeout time.Duration
maxDeadline time.Time
closeCanceler context.CancelFunc
idleTimeout time.Duration
handshakeDeadline time.Time
maxDeadline time.Time
closeCanceler context.CancelFunc
}

func (c *serverConn) Write(p []byte) (n int, err error) {
c.updateDeadline()
if c.idleTimeout > 0 {
c.updateDeadline()
}
n, err = c.Conn.Write(p)
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
c.closeCanceler()
@@ -24,7 +27,9 @@ func (c *serverConn) Write(p []byte) (n int, err error) {
}

func (c *serverConn) Read(b []byte) (n int, err error) {
c.updateDeadline()
if c.idleTimeout > 0 {
c.updateDeadline()
}
n, err = c.Conn.Read(b)
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
c.closeCanceler()
@@ -41,15 +46,18 @@ func (c *serverConn) Close() (err error) {
}

func (c *serverConn) updateDeadline() {
switch {
case c.idleTimeout > 0:
deadline := c.maxDeadline

if !c.handshakeDeadline.IsZero() && (deadline.IsZero() || c.handshakeDeadline.Before(deadline)) {
deadline = c.handshakeDeadline
}

if c.idleTimeout > 0 {
idleDeadline := time.Now().Add(c.idleTimeout)
if idleDeadline.Unix() < c.maxDeadline.Unix() {
c.Conn.SetDeadline(idleDeadline)
return
if deadline.IsZero() || idleDeadline.Before(deadline) {
deadline = idleDeadline
}
fallthrough
default:
c.Conn.SetDeadline(c.maxDeadline)
}

c.Conn.SetDeadline(deadline)
}
31 changes: 26 additions & 5 deletions context.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import (
"context"
"encoding/hex"
"net"
"sync"

gossh "golang.org/x/crypto/ssh"
)
@@ -48,7 +49,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.Conn.
// 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.
@@ -59,9 +60,11 @@ 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.
// 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
@@ -90,11 +93,15 @@ type Context interface {

type sshContext struct {
context.Context
*sync.Mutex

values map[interface{}]interface{}
valuesMu sync.Mutex
}

func newContext(srv *Server) (*sshContext, context.CancelFunc) {
innerCtx, cancel := context.WithCancel(context.Background())
ctx := &sshContext{innerCtx}
ctx := &sshContext{Context: innerCtx, Mutex: &sync.Mutex{}, values: make(map[interface{}]interface{})}
ctx.SetValue(ContextKeyServer, srv)
perms := &Permissions{&gossh.Permissions{}}
ctx.SetValue(ContextKeyPermissions, perms)
@@ -115,8 +122,19 @@ func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) {
ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr())
}

func (ctx *sshContext) Value(key interface{}) interface{} {
ctx.valuesMu.Lock()
defer ctx.valuesMu.Unlock()
if v, ok := ctx.values[key]; ok {
return v
}
return ctx.Context.Value(key)
}

func (ctx *sshContext) SetValue(key, value interface{}) {
ctx.Context = context.WithValue(ctx.Context, key, value)
ctx.valuesMu.Lock()
defer ctx.valuesMu.Unlock()
ctx.values[key] = value
}

func (ctx *sshContext) User() string {
@@ -136,7 +154,10 @@ func (ctx *sshContext) ServerVersion() string {
}

func (ctx *sshContext) RemoteAddr() net.Addr {
return ctx.Value(ContextKeyRemoteAddr).(net.Addr)
if addr, ok := ctx.Value(ContextKeyRemoteAddr).(net.Addr); ok {
return addr
}
return nil
}

func (ctx *sshContext) LocalAddr() net.Addr {
40 changes: 39 additions & 1 deletion context_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package ssh

import "testing"
import (
"testing"
"time"
)

func TestSetPermissions(t *testing.T) {
t.Parallel()
@@ -45,3 +48,38 @@ func TestSetValue(t *testing.T) {
t.Fatal(err)
}
}

func TestSetValueConcurrency(t *testing.T) {
ctx, cancel := newContext(nil)
defer cancel()

go func() {
for { // use a loop to access context.Context functions to make sure they are thread-safe with SetValue
_, _ = ctx.Deadline()
_ = ctx.Err()
_ = ctx.Value("foo")
select {
case <-ctx.Done():
break
default:
}
}
}()
ctx.SetValue("bar", -1) // a context value which never changes
now := time.Now()
var cnt int64
go func() {
for time.Since(now) < 100*time.Millisecond {
cnt++
ctx.SetValue("foo", cnt) // a context value which changes a lot
}
cancel()
}()
<-ctx.Done()
if ctx.Value("foo") != cnt {
t.Fatal("context.Value(foo) doesn't match latest SetValue")
}
if ctx.Value("bar") != -1 {
t.Fatal("context.Value(bar) doesn't match latest SetValue")
}
}
2 changes: 0 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/*
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.
@@ -42,6 +41,5 @@ 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
4 changes: 2 additions & 2 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ package ssh_test

import (
"io"
"io/ioutil"
"os"

"github.com/gliderlabs/ssh"
)
@@ -28,7 +28,7 @@ func ExampleNoPty() {
func ExamplePublicKeyAuth() {
ssh.ListenAndServe(":2222", nil,
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
data, _ := ioutil.ReadFile("/path/to/allowed/key.pub")
data, _ := os.ReadFile("/path/to/allowed/key.pub")
allowed, _, _, _, _ := ssh.ParseAuthorizedKey(data)
return ssh.KeysEqual(key, allowed)
}),
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/gliderlabs/ssh

go 1.20

require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
golang.org/x/crypto v0.31.0
)

require golang.org/x/sys v0.28.0 // indirect
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
Loading