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: eeeeethan2333/ssh
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
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

Commits on Feb 13, 2020

  1. Fix typo in Session docs

    Vladimír Chalupecký committed Feb 13, 2020
    Copy the full SHA
    a7de8ac View commit details

Commits on Oct 7, 2020

  1. Register chan to Session to listen for break requests (gliderlabs#141)

    Co-authored-by: Jacob Meisler <meislerj@amazon.com>
    meislerj and Jacob Meisler authored Oct 7, 2020
    Copy the full SHA
    fb34512 View commit details

Commits on Dec 12, 2020

  1. Add ConnectionFailedCallback to enable reporting of failed connection

    Signed-off-by: Andrew Thornton <art27@cantab.net>
    zeripath committed Dec 12, 2020
    Copy the full SHA
    98ce6bf View commit details

Commits on Feb 1, 2021

  1. Fix: KeyboardInteractive Login

    + Include KeyboardInteractive in decision if NoClientAuth should be performed
    + Add option for KeyboardInteractive
    Lemonn authored and belak committed Feb 1, 2021
    Copy the full SHA
    1593226 View commit details

Commits on Feb 16, 2021

  1. Copy the full SHA
    30ec06d View commit details

Commits on Jun 28, 2021

  1. Merge pull request gliderlabs#143 from zeripath/connection-failed-cal…

    …lback
    
    Add ConnectionFailedCallback to enable reporting of failed connection
    belak authored Jun 28, 2021
    Copy the full SHA
    4204f8c View commit details
  2. Add go mod files (gliderlabs#153)

    * Add go mod files
    * Fix circle ci
    * Update minimum supported version to 1.12
    
    Co-authored-by: Kaleb Elwert <kaleb@coded.io>
    rumpelsepp and belak authored Jun 28, 2021
    Copy the full SHA
    bf02c41 View commit details
  3. Update go.mod

    belak committed Jun 28, 2021
    Copy the full SHA
    3427354 View commit details
  4. Prevent Context.RemoteAddr() from exploding when called from ConnCall…

    …back
    
    When calling ctx.RemoteAddr() within ConnCallback one gets a panic due to
    unsafe casting within the accessor. I understand it's not a valid scenario
    as such, but I accidentally called a logger that expected that checked
    for RemoteAddr() in ssh.Context
    
    panic: interface conversion: interface is nil, not net.Addr
    
    Signed-off-by: Alejandro Mery <amery@geeks.cl>
    amery authored and belak committed Jun 28, 2021
    Copy the full SHA
    c7b4a1b View commit details
  5. Merge pull request gliderlabs#134 from vladimir-ch/session-docs-typo

    Fix typo in Session docs
    belak authored Jun 28, 2021
    Copy the full SHA
    c9fc441 View commit details

Commits on Jan 6, 2022

  1. feat: return ssh.Context

    Signed-off-by: Carlos A Becker <caarlos0@gmail.com>
    caarlos0 committed Jan 6, 2022
    Copy the full SHA
    dd71f10 View commit details

Commits on May 9, 2022

  1. Merge pull request gliderlabs#168 from caarlos0/ctx

    feat: return ssh.Context
    belak authored May 9, 2022
    Copy the full SHA
    777ab34 View commit details

Commits on Aug 29, 2022

  1. Copy the full SHA
    334c16f View commit details

Commits on Aug 31, 2022

  1. Copy the full SHA
    5cf2edf View commit details
  2. Merge pull request gliderlabs#180 from gustavosbarreto/update-crypto-ssh

    Update golang.org/x/crypto/ssh
    belak authored Aug 31, 2022
    Copy the full SHA
    0f80af4 View commit details

Commits on Oct 1, 2022

  1. add sftp-server example

    jm33-m0 committed Oct 1, 2022
    Copy the full SHA
    3cef403 View commit details

Commits on Oct 25, 2022

  1. Merge pull request gliderlabs#182 from jm33-m0/master

    add sftp-server example
    gustavosbarreto authored Oct 25, 2022
    Copy the full SHA
    db09465 View commit details

Commits on Feb 25, 2023

  1. Bump golang.org/x/net from 0.0.0-20220826154423-83b083e8dc8b to 0.7.0

    Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20220826154423-83b083e8dc8b to 0.7.0.
    - [Release notes](https://github.com/golang/net/releases)
    - [Commits](https://github.com/golang/net/commits/v0.7.0)
    
    ---
    updated-dependencies:
    - dependency-name: golang.org/x/net
      dependency-type: indirect
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    dependabot[bot] authored Feb 25, 2023
    Copy the full SHA
    8aa15f7 View commit details

Commits on Mar 16, 2023

  1. Merge pull request gliderlabs#198 from gliderlabs/dependabot/go_modul…

    …es/golang.org/x/net-0.7.0
    
    Bump golang.org/x/net from 0.0.0-20220826154423-83b083e8dc8b to 0.7.0
    gustavosbarreto authored Mar 16, 2023
    Copy the full SHA
    cf1ec7e View commit details

Commits on Aug 12, 2023

  1. Copy the full SHA
    02f9d57 View commit details

Commits on Sep 27, 2023

  1. Merge pull request gliderlabs#211 from wxiaoguang/fix-data-race

    Make sshContext thread safe and fix the data race bug
    gustavosbarreto authored Sep 27, 2023
    Copy the full SHA
    ece6c79 View commit details

Commits on Dec 18, 2023

  1. Copy the full SHA
    67def36 View commit details

Commits on Dec 19, 2023

  1. Merge pull request gliderlabs#217 from adrianosela/master

    [CVE-2023-48795] Bump golang.org/x/crypto to v0.17.0
    belak authored Dec 19, 2023
    Copy the full SHA
    7de97f6 View commit details

Commits on Jan 4, 2024

  1. Copy the full SHA
    973b62f View commit details
  2. Merge pull request gliderlabs#221 from charmbracelet/banner-upstream

    feat: allow to set a server banner
    gustavosbarreto authored Jan 4, 2024
    Copy the full SHA
    8867fb1 View commit details

Commits on Jan 5, 2024

  1. Copy the full SHA
    c7a15f4 View commit details

Commits on Mar 18, 2024

  1. fix: bannerhandler

    caarlos0 authored and gustavosbarreto committed Mar 18, 2024
    Copy the full SHA
    c1393df View commit details
  2. Merge pull request gliderlabs#222 from aymanbagabas/ioutil

    fix: remove ioutil deprecations
    gustavosbarreto authored Mar 18, 2024
    Copy the full SHA
    ee51862 View commit details
  3. Copy the full SHA
    adec695 View commit details

Commits on Apr 20, 2024

  1. time out a client connection if a successful handshake has not happen…

    …ed within the duration HandshakeTimeout
    aderouineau authored and aderouineau-amz committed Apr 20, 2024
    Copy the full SHA
    2326046 View commit details

Commits on Dec 12, 2024

  1. Copy the full SHA
    a8ecd3e View commit details
  2. Bump minimum supported Go version to 1.20

    This is a requirement, now that x/crypto requires 1.20 or above and we
    had to update x/crypto for CVE-2024-45337
    belak committed Dec 12, 2024
    Copy the full SHA
    d137aad View commit details

Commits on Jan 27, 2025

  1. Copy the full SHA
    909fa95 View commit details
Showing with 346 additions and 40 deletions.
  1. +1 −1 _examples/ssh-pty/pty.go
  2. +42 −0 _examples/ssh-sftpserver/sftp.go
  3. +2 −2 agent.go
  4. +3 −3 circle.yml
  5. +21 −13 conn.go
  6. +20 −3 context.go
  7. +39 −1 context_test.go
  8. +2 −2 example_test.go
  9. +10 −0 go.mod
  10. +7 −0 go.sum
  11. +9 −2 options.go
  12. +28 −5 server.go
  13. +34 −0 server_test.go
  14. +25 −4 session.go
  15. +93 −0 session_test.go
  16. +8 −2 ssh.go
  17. +2 −2 tcpip_test.go
2 changes: 1 addition & 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) {
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.maxDeadline.IsZero() {
c.Conn.SetDeadline(idleDeadline)
return
if deadline.IsZero() || idleDeadline.Before(deadline) {
deadline = idleDeadline
}
fallthrough
default:
c.Conn.SetDeadline(c.maxDeadline)
}

c.Conn.SetDeadline(deadline)
}
23 changes: 20 additions & 3 deletions context.go
Original file line number Diff line number Diff line change
@@ -94,11 +94,14 @@ 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, &sync.Mutex{}}
ctx := &sshContext{Context: innerCtx, Mutex: &sync.Mutex{}, values: make(map[interface{}]interface{})}
ctx.SetValue(ContextKeyServer, srv)
perms := &Permissions{&gossh.Permissions{}}
ctx.SetValue(ContextKeyPermissions, perms)
@@ -119,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 {
@@ -140,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")
}
}
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=
11 changes: 9 additions & 2 deletions options.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package ssh

import (
"io/ioutil"
"os"

gossh "golang.org/x/crypto/ssh"
)
@@ -26,7 +26,7 @@ func PublicKeyAuth(fn PublicKeyHandler) Option {
// from a PEM file at filepath.
func HostKeyFile(filepath string) Option {
return func(srv *Server) error {
pemBytes, err := ioutil.ReadFile(filepath)
pemBytes, err := os.ReadFile(filepath)
if err != nil {
return err
}
@@ -42,6 +42,13 @@ 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 {
33 changes: 28 additions & 5 deletions server.go
Original file line number Diff line number Diff line change
@@ -37,7 +37,9 @@ type Server struct {
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
Banner string // server banner

BannerHandler BannerHandler // server banner handler, overrides Banner
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
@@ -48,8 +50,11 @@ type Server struct {
ServerConfigCallback ServerConfigCallback // callback for configuring detailed SSH options
SessionRequestCallback SessionRequestCallback // callback for allowing or denying SSH sessions

IdleTimeout time.Duration // connection timeout when no activity, none if empty
MaxTimeout time.Duration // absolute connection timeout, none if empty
ConnectionFailedCallback ConnectionFailedCallback // callback to report connection failures

HandshakeTimeout time.Duration // connection timeout until successful handshake, none if empty
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
@@ -124,12 +129,23 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
for _, signer := range srv.HostSigners {
config.AddHostKey(signer)
}
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil {
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.Banner != "" {
config.BannerCallback = func(_ gossh.ConnMetadata) string {
return srv.Banner
}
}
if srv.BannerHandler != nil {
config.BannerCallback = func(conn gossh.ConnMetadata) string {
applyConnMetadata(ctx, conn)
return srv.BannerHandler(ctx)
}
}
if srv.PasswordHandler != nil {
config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
applyConnMetadata(ctx, conn)
@@ -275,13 +291,20 @@ func (srv *Server) HandleConn(newConn net.Conn) {
if srv.MaxTimeout > 0 {
conn.maxDeadline = time.Now().Add(srv.MaxTimeout)
}
if srv.HandshakeTimeout > 0 {
conn.handshakeDeadline = time.Now().Add(srv.HandshakeTimeout)
}
conn.updateDeadline()
defer conn.Close()
sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx))
if err != nil {
// TODO: trigger event callback
if srv.ConnectionFailedCallback != nil {
srv.ConnectionFailedCallback(conn, err)
}
return
}

conn.handshakeDeadline = time.Time{}
conn.updateDeadline()
srv.trackConn(sshConn, true)
defer srv.trackConn(sshConn, false)

34 changes: 34 additions & 0 deletions server_test.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"io"
"net"
"testing"
"time"
)
@@ -124,3 +125,36 @@ func TestServerClose(t *testing.T) {
return
}
}

func TestServerHandshakeTimeout(t *testing.T) {
l := newLocalListener()

s := &Server{
HandshakeTimeout: time.Millisecond,
}
go func() {
if err := s.Serve(l); err != nil {
t.Error(err)
}
}()

conn, err := net.Dial("tcp", l.Addr().String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()

ch := make(chan struct{})
go func() {
defer close(ch)
io.Copy(io.Discard, conn)
}()

select {
case <-ch:
return
case <-time.After(time.Second):
t.Fatal("client connection was not force-closed")
return
}
}
29 changes: 25 additions & 4 deletions session.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ package ssh

import (
"bytes"
"context"
"errors"
"fmt"
"net"
@@ -14,7 +13,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
// cypto/ssh.
// crypto/ssh.
//
// When Command() returns an empty slice, the user requested a shell. Otherwise
// the user is performing an exec with those command arguments.
@@ -60,7 +59,7 @@ type Session interface {
//
// The context is canceled when the client's connection closes or I/O
// operation fails.
Context() context.Context
Context() Context

// Permissions returns a copy of the Permissions object that was available for
// setup in the auth handlers via the Context.
@@ -77,6 +76,12 @@ 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
@@ -119,6 +124,7 @@ type session struct {
ctx Context
sigCh chan<- Signal
sigBuf []Signal
breakCh chan<- bool
}

func (sess *session) Write(p []byte) (n int, err error) {
@@ -152,7 +158,7 @@ func (sess *session) Permissions() Permissions {
return *perms
}

func (sess *session) Context() context.Context {
func (sess *session) Context() Context {
return sess.ctx
}

@@ -221,6 +227,12 @@ 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 {
@@ -344,6 +356,15 @@ 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)
93 changes: 93 additions & 0 deletions session_test.go
Original file line number Diff line number Diff line change
@@ -343,3 +343,96 @@ func TestSignals(t *testing.T) {
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)
}
}
10 changes: 8 additions & 2 deletions ssh.go
Original file line number Diff line number Diff line change
@@ -35,6 +35,9 @@ type Option func(*Server) error
// Handler is a callback for handling established SSH sessions.
type Handler func(Session)

// BannerHandler is a callback for displaying the server banner.
type BannerHandler func(ctx Context) string

// PublicKeyHandler is a callback for performing public key authentication.
type PublicKeyHandler func(ctx Context, key PublicKey) bool

@@ -64,6 +67,10 @@ type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort u
// 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
@@ -111,8 +118,7 @@ func Handle(handler 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
// avoid panic if one of the keys is nil, return false instead
if ak == nil || bk == nil {
return false
}
4 changes: 2 additions & 2 deletions tcpip_test.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ package ssh

import (
"bytes"
"io/ioutil"
"io"
"net"
"strconv"
"strings"
@@ -58,7 +58,7 @@ func TestLocalPortForwardingWorks(t *testing.T) {
if err != nil {
t.Fatalf("Error connecting to %v: %v", l.Addr().String(), err)
}
result, err := ioutil.ReadAll(conn)
result, err := io.ReadAll(conn)
if err != nil {
t.Fatal(err)
}