Skip to content

Commit

Permalink
feat: allocate real pty
Browse files Browse the repository at this point in the history
This fills the gap of allocating a real PTY when a session asks for one.
It uses [go-pty](https://github.com/aymanbagabas/go-pty) to support PTYs
for both Unix and Windows OSs. It also adds two new PtyCallback options,
  `AllocatePty` opens a new PTY for the session when requested.
  `EmulatePty` preserves the current behavior of the library.

Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
  • Loading branch information
aymanbagabas committed Nov 29, 2023
1 parent 1a051f8 commit 669b249
Show file tree
Hide file tree
Showing 6 changed files with 447 additions and 9 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ go 1.12

require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
golang.org/x/term v0.5.0 // indirect
github.com/aymanbagabas/go-pty v0.2.0
golang.org/x/crypto v0.14.0
)
374 changes: 369 additions & 5 deletions go.sum

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,27 @@ func WrapConn(fn ConnCallback) Option {
return nil
}
}

// EmulatePty returns a functional option that sets PtyCallback on the server
// to emulate a "fake" PTY by using PtyWriter.
func EmulatePty() Option {
return func(srv *Server) error {
srv.PtyCallback = func(_ Context, pty Pty) bool {
pty.emulate = true
return true
}
return nil
}
}

// AllocatePty returns a functional option that sets PtyCallback on the server
// to allocate a PTY for sessions that request it.
func AllocatePty() Option {
return func(srv *Server) error {
srv.PtyCallback = func(_ Context, pty Pty) bool {
err := pty.allocate()
return err == nil
}
return nil
}
}
22 changes: 22 additions & 0 deletions pty.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package ssh
import (
"bytes"
"io"

"github.com/aymanbagabas/go-pty"
)

// NewPtyWriter creates a writer that handles when the session has a active
Expand Down Expand Up @@ -55,3 +57,23 @@ func (rw readWriterDelegate) Read(p []byte) (n int, err error) {
func (rw readWriterDelegate) Write(p []byte) (n int, err error) {
return rw.w.Write(p)
}

// allocate is a helper function to allocate a pty session.
// It returns a Pty object that can be used to run a command on a tty.
// If this fails, use NewPtyReadWriter to _emulate_ a tty.
func (p *Pty) allocate() error {
tty, err := pty.New()
if err != nil {
return err
}

p.Pty = tty

if err := pty.ApplyTerminalModes(int(tty.Fd()),
p.Window.Width, p.Window.Height, p.Modes,
); err != nil {
return err
}

return nil
}
23 changes: 21 additions & 2 deletions session.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io"
"log"
"net"
"sync"

Expand Down Expand Up @@ -128,14 +129,14 @@ type session struct {
}

func (sess *session) Stderr() io.ReadWriter {
if sess.pty != nil {
if sess.pty != nil && sess.pty.emulate {
return NewPtyReadWriter(sess.Channel.Stderr())
}
return sess.Channel.Stderr()
}

func (sess *session) Write(p []byte) (int, error) {
if sess.pty != nil {
if sess.pty != nil && sess.pty.emulate {
return NewPtyWriter(sess.Channel).Write(p)
}
return sess.Channel.Write(p)
Expand Down Expand Up @@ -331,6 +332,21 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
continue
}
}
if err := ptyReq.allocate(); err != nil {
req.Reply(false, nil)
continue
}
if ptyReq.Pty != nil {
log.Printf("pty allocated: %s", ptyReq.Pty.Name())
sess.env = append(sess.env,
fmt.Sprintf("TERM=%s", ptyReq.Term),
fmt.Sprintf("SSH_TTY=%s", ptyReq.Pty.Name()),
)
// input to pty
go io.Copy(ptyReq.Pty, sess) // nolint: errcheck
// pty to output
go io.Copy(sess, ptyReq.Pty) // nolint: errcheck
}
sess.pty = &ptyReq
sess.winch = make(chan Window, 1)
sess.winch <- ptyReq.Window
Expand All @@ -348,6 +364,9 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
if ok {
sess.pty.Window = win
sess.winch <- win
if sess.pty.Pty != nil {
sess.pty.Pty.Resize(win.Width, win.Height)
}
}
req.Reply(ok, nil)
case agentRequestType:
Expand Down
9 changes: 9 additions & 0 deletions ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/subtle"
"net"

"github.com/aymanbagabas/go-pty"
gossh "golang.org/x/crypto/ssh"
)

Expand Down Expand Up @@ -91,6 +92,14 @@ type Window struct {

// Pty represents a PTY request and configuration.
type Pty struct {
// If this is true, the server will emulate a "fake" Pty by using
// PtyWriter.
emulate bool

// Pty is the actual pty allocated. It is nil if the request did not
// allocated a pty.
pty.Pty

// Term is the TERM environment variable value.
Term string

Expand Down

0 comments on commit 669b249

Please sign in to comment.