diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6a61a1b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,84 @@ +# Main CI workflow. Runs build, vet, and tests on PRs and merged commits. +name: CI + +on: + push: + branches: + - "main" + pull_request: + # all PRs on all branches + merge_group: + branches: + - "main" + +concurrency: + # For PRs, later CI runs preempt previous ones. e.g. a force push on a PR + # cancels running CI jobs and starts all new ones. + # + # For non-PR pushes, concurrency.group needs to be unique for every distinct + # CI run we want to have happen. Use run_id, which in practice means all + # non-PR CI runs will be allowed to run without preempting each other. + group: ${{ github.workflow }}-$${{ github.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + test: + strategy: + fail-fast: false # don't abort the entire matrix if one element fails + matrix: + include: + - goarch: amd64 + - goarch: amd64 + buildflags: "-race" + - goarch: "386" # thanks yaml + runs-on: ubuntu-24.04 + steps: + - name: checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: setup Go + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version-file: go.mod + + - name: build all + if: matrix.buildflags == '' # skip on race builder + run: go build ./... + env: + GOARCH: ${{ matrix.goarch }} + + - name: vet + if: matrix.buildflags == '' && matrix.goarch == 'amd64' + run: go vet ./... + + - name: test all + run: go test ${{ matrix.buildflags }} ./... + env: + GOARCH: ${{ matrix.goarch }} + + - name: check that no tracked files changed + run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1) + + - name: check that no new files were added + run: | + # Note: The "error: pathspec..." you see below is normal! + # In the success case in which there are no new untracked files, + # git ls-files complains about the pathspec not matching anything. + # That's OK. It's not worth the effort to suppress. Please ignore it. + if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*' + then + echo "Build/test created untracked files in the repo (file names above)." + exit 1 + fi + + check_mergeability: + if: always() + runs-on: ubuntu-24.04 + needs: + - test + steps: + - name: Decide if change is okay to merge + if: github.event_name != 'push' + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/README.md b/README.md index fbb2fbb..ab74665 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,33 @@ -# gliderlabs/ssh +# tailscale/gliderssh -[![GoDoc](https://godoc.org/github.com/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh) -[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh) -[![Go Report Card](https://goreportcard.com/badge/github.com/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh) -[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors) -[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com) -[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312) +[![Go Reference](https://pkg.go.dev/badge/github.com/tailscale/gliderssh.svg)](https://pkg.go.dev/github.com/tailscale/gliderssh) +[![Go Report Card](https://goreportcard.com/badge/github.com/tailscale/gliderssh)](https://goreportcard.com/report/github.com/tailscale/gliderssh) -> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member +This is a [Tailscale](https://tailscale.com) fork of [gliderlabs/ssh](https://github.com/gliderlabs/ssh). This Go package wraps the [crypto/ssh -package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for +package](https://pkg.go.dev/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 +package main - import ( - "github.com/gliderlabs/ssh" - "io" - "log" - ) +import ( + "io" + "log" - func main() { - ssh.Handle(func(s ssh.Session) { - io.WriteString(s, "Hello world\n") - }) + ssh "github.com/tailscale/gliderssh" +) - log.Fatal(ssh.ListenAndServe(":2222", nil)) - } +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 @@ -40,57 +35,13 @@ A bunch of great examples are in the `_examples` directory. ## Usage -[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh) +[See Go reference.](https://pkg.go.dev/github.com/tailscale/gliderssh) ## 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)] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ## License [BSD](LICENSE) diff --git a/_examples/ssh-docker/README.md b/_examples/ssh-docker/README.md index decee02..6454080 100644 --- a/_examples/ssh-docker/README.md +++ b/_examples/ssh-docker/README.md @@ -2,10 +2,10 @@ Run docker containers over SSH. You can even pipe things into them too! # Installation / Prep -We're going to build JQ as an SSH service using the Glider Labs SSH package. If you haven't installed GoLang and docker yet, see the doc's for help getting your environment setup. +We're going to build JQ as an SSH service using the gliderssh package. If you haven't installed Go and docker yet, see the docs for help getting your environment setup. -Install the Glider Labs SSH package -`go get github.com/gliderlabs/ssh` +Install the gliderssh package +`go get github.com/tailscale/gliderssh` Build the example docker container with `docker build --rm -t jq .` @@ -88,7 +88,7 @@ JQ's help text! It's working! Now let's pipe some json into our SSH service an ``` # Conclusion -We built JQ as a service over SSH in Go using the Glider Labs SSH package. We showed how you can run docker containers through the service as well as how to pipe stuff into your SSH service. +We built JQ as a service over SSH in Go using the gliderssh package. We showed how you can run docker containers through the service as well as how to pipe stuff into your SSH service. # Troubleshooting diff --git a/_examples/ssh-docker/docker.go b/_examples/ssh-docker/docker.go index 6c2b347..e3d2107 100644 --- a/_examples/ssh-docker/docker.go +++ b/_examples/ssh-docker/docker.go @@ -10,7 +10,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func main() { diff --git a/_examples/ssh-forwardagent/forwardagent.go b/_examples/ssh-forwardagent/forwardagent.go index 227693f..0b474dd 100644 --- a/_examples/ssh-forwardagent/forwardagent.go +++ b/_examples/ssh-forwardagent/forwardagent.go @@ -5,7 +5,7 @@ import ( "log" "os/exec" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func main() { diff --git a/_examples/ssh-pty/pty.go b/_examples/ssh-pty/pty.go index 80bfac5..0a205cc 100644 --- a/_examples/ssh-pty/pty.go +++ b/_examples/ssh-pty/pty.go @@ -9,7 +9,7 @@ import ( "syscall" "unsafe" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" "github.com/creack/pty" ) diff --git a/_examples/ssh-publickey/public_key.go b/_examples/ssh-publickey/public_key.go index 453ce04..9ca5727 100644 --- a/_examples/ssh-publickey/public_key.go +++ b/_examples/ssh-publickey/public_key.go @@ -5,7 +5,7 @@ import ( "io" "log" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" gossh "golang.org/x/crypto/ssh" ) diff --git a/_examples/ssh-remoteforward/portforward.go b/_examples/ssh-remoteforward/portforward.go index 2ce866f..a132cf9 100644 --- a/_examples/ssh-remoteforward/portforward.go +++ b/_examples/ssh-remoteforward/portforward.go @@ -4,7 +4,7 @@ import ( "io" "log" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func main() { diff --git a/_examples/ssh-simple/simple.go b/_examples/ssh-simple/simple.go index d2bcff1..a102ab4 100644 --- a/_examples/ssh-simple/simple.go +++ b/_examples/ssh-simple/simple.go @@ -5,7 +5,7 @@ import ( "io" "log" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func main() { diff --git a/_examples/ssh-timeouts/timeouts.go b/_examples/ssh-timeouts/timeouts.go index 1dba09f..1648918 100644 --- a/_examples/ssh-timeouts/timeouts.go +++ b/_examples/ssh-timeouts/timeouts.go @@ -4,7 +4,7 @@ import ( "log" "time" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) var ( diff --git a/circle.yml b/circle.yml deleted file mode 100644 index c97103d..0000000 --- a/circle.yml +++ /dev/null @@ -1,26 +0,0 @@ -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.12: - docker: - - image: golang:1.12 - 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.12 diff --git a/doc.go b/doc.go index 5a10393..d139191 100644 --- a/doc.go +++ b/doc.go @@ -10,29 +10,29 @@ 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") - }) + ssh.Handle(func(s ssh.Session) { + io.WriteString(s, "Hello world\n") + }) - log.Fatal(ssh.ListenAndServe(":2222", nil)) + 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"))) + 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) + s := &ssh.Server{ + Addr: ":2222", + Handler: sessionHandler, + PublicKeyHandler: authHandler, + } + s.AddHostKey(hostKeySigner) - log.Fatal(s.ListenAndServe()) + log.Fatal(s.ListenAndServe()) This package automatically handles basic SSH requests like setting environment variables, requesting PTY, and changing window size. These requests are diff --git a/example_test.go b/example_test.go index 972d3ef..fa9bafa 100644 --- a/example_test.go +++ b/example_test.go @@ -1,10 +1,11 @@ package ssh_test import ( + "errors" "io" - "io/ioutil" + "os" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func ExampleListenAndServe() { @@ -27,10 +28,19 @@ 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") - allowed, _, _, _, _ := ssh.ParseAuthorizedKey(data) - return ssh.KeysEqual(key, allowed) + 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 }), ) } diff --git a/go.mod b/go.mod index eee9a4f..d43753f 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ -module github.com/tailscale/ssh +module github.com/tailscale/gliderssh -go 1.12 +go 1.26 require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be - golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e - golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect + golang.org/x/crypto v0.31.0 ) + +require golang.org/x/sys v0.28.0 // indirect diff --git a/go.sum b/go.sum index e283b5f..1163128 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,8 @@ 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-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= diff --git a/server.go b/server.go index be4355e..5c17d2f 100644 --- a/server.go +++ b/server.go @@ -38,9 +38,11 @@ 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 + BannerHandler BannerHandler // server banner handler, overrides Banner 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 @@ -144,8 +146,8 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig { if srv.PublicKeyHandler != nil { config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) { applyConnMetadata(ctx, conn) - if ok := srv.PublicKeyHandler(ctx, key); !ok { - return ctx.Permissions().Permissions, fmt.Errorf("permission denied") + if err := srv.PublicKeyHandler(ctx, key); err != nil { + return ctx.Permissions().Permissions, err } ctx.SetValue(ContextKeyPublicKey, key) return ctx.Permissions().Permissions, nil @@ -160,6 +162,15 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig { 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 } diff --git a/server_test.go b/server_test.go index 8028a3a..498621e 100644 --- a/server_test.go +++ b/server_test.go @@ -40,7 +40,7 @@ func TestServerShutdown(t *testing.T) { go func() { err := s.Serve(l) if err != nil && err != ErrServerClosed { - t.Fatal(err) + t.Error(err) } }() sessDone := make(chan struct{}) @@ -51,10 +51,11 @@ func TestServerShutdown(t *testing.T) { var stdout bytes.Buffer sess.Stdout = &stdout if err := sess.Run(""); err != nil { - t.Fatal(err) + t.Error(err) + return } if !bytes.Equal(stdout.Bytes(), testBytes) { - t.Fatalf("expected = %s; got %s", testBytes, stdout.Bytes()) + t.Errorf("expected = %s; got %s", testBytes, stdout.Bytes()) } }() @@ -63,7 +64,7 @@ func TestServerShutdown(t *testing.T) { defer close(srvDone) err := s.Shutdown(context.Background()) if err != nil { - t.Fatal(err) + t.Error(err) } }() @@ -89,7 +90,7 @@ func TestServerClose(t *testing.T) { go func() { err := s.Serve(l) if err != nil && err != ErrServerClosed { - t.Fatal(err) + t.Error(err) } }() @@ -102,14 +103,14 @@ func TestServerClose(t *testing.T) { defer close(clientDoneChan) <-closeDoneChan if err := sess.Run(""); err != nil && err != io.EOF { - t.Fatal(err) + t.Error(err) } }() go func() { err := s.Close() if err != nil { - t.Fatal(err) + t.Error(err) } close(closeDoneChan) }() diff --git a/session_test.go b/session_test.go index c6ce617..a83ab19 100644 --- a/session_test.go +++ b/session_test.go @@ -230,9 +230,9 @@ func TestPty(t *testing.T) { func TestPtyResize(t *testing.T) { t.Parallel() - winch0 := Window{40, 80} - winch1 := Window{80, 160} - winch2 := Window{20, 40} + winch0 := Window{Width: 40, Height: 80} + winch1 := Window{Width: 80, Height: 160, WidthPixels: 640, HeightPixels: 1280} + winch2 := Window{Width: 20, Height: 40, WidthPixels: 160, HeightPixels: 320} winches := make(chan Window) done := make(chan bool) session, _, cleanup := newTestSession(t, &Server{ @@ -241,8 +241,15 @@ func TestPtyResize(t *testing.T) { if !isPty { t.Fatalf("expected pty but none requested") } - if ptyReq.Window != winch0 { - t.Fatalf("expected window %#v but got %#v", winch0, ptyReq.Window) + // RequestPty computes pixel dimensions from char dimensions, + // so we only check Width/Height for the initial pty request. + if ptyReq.Window.Width != winch0.Width || ptyReq.Window.Height != winch0.Height { + t.Fatalf("expected window size %dx%d but got %dx%d", + winch0.Width, winch0.Height, + ptyReq.Window.Width, ptyReq.Window.Height) + } + if ptyReq.Window.WidthPixels == 0 || ptyReq.Window.HeightPixels == 0 { + t.Fatalf("expected non-zero pixel dimensions in pty request, got %#v", ptyReq.Window) } for win := range winCh { winches <- win @@ -259,11 +266,19 @@ func TestPtyResize(t *testing.T) { t.Fatalf("expected nil but got %v", err) } gotWinch := <-winches - if gotWinch != winch0 { - t.Fatalf("expected window %#v but got %#v", winch0, gotWinch) + if gotWinch.Width != winch0.Width || gotWinch.Height != winch0.Height { + t.Fatalf("expected window size %dx%d but got %dx%d", + winch0.Width, winch0.Height, + gotWinch.Width, gotWinch.Height) + } + if gotWinch.WidthPixels == 0 || gotWinch.HeightPixels == 0 { + t.Fatalf("expected non-zero pixel dimensions, got %#v", gotWinch) + } + // winch1 — window-change sends all 4 fields per RFC 4254 section 6.7 + winchMsg := struct{ W, H, Wpx, Hpx uint32 }{ + uint32(winch1.Width), uint32(winch1.Height), + uint32(winch1.WidthPixels), uint32(winch1.HeightPixels), } - // 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") @@ -273,7 +288,10 @@ func TestPtyResize(t *testing.T) { t.Fatalf("expected window %#v but got %#v", winch1, gotWinch) } // winch2 - winchMsg = struct{ w, h uint32 }{uint32(winch2.Width), uint32(winch2.Height)} + winchMsg = struct{ W, H, Wpx, Hpx uint32 }{ + uint32(winch2.Width), uint32(winch2.Height), + uint32(winch2.WidthPixels), uint32(winch2.HeightPixels), + } ok, err = session.SendRequest("window-change", true, gossh.Marshal(&winchMsg)) if err == nil && !ok { t.Fatalf("unexpected error or bad reply on send request") diff --git a/ssh.go b/ssh.go index 8bb02a3..4a77013 100644 --- a/ssh.go +++ b/ssh.go @@ -35,8 +35,13 @@ 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 +type PublicKeyHandler func(ctx Context, key PublicKey) error + +type NoClientAuthHandler func(ctx Context) error // PasswordHandler is a callback for performing password authentication. type PasswordHandler func(ctx Context, password string) bool