Skip to content

Commit 1aa5d86

Browse files
committed
feat: allocate real pty
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 <[email protected]>
1 parent 1a051f8 commit 1aa5d86

File tree

6 files changed

+448
-9
lines changed

6 files changed

+448
-9
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ go 1.12
44

55
require (
66
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
7-
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
8-
golang.org/x/term v0.5.0 // indirect
7+
github.com/aymanbagabas/go-pty v0.2.0
8+
golang.org/x/crypto v0.14.0
99
)

go.sum

Lines changed: 369 additions & 5 deletions
Large diffs are not rendered by default.

options.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,27 @@ func WrapConn(fn ConnCallback) Option {
8282
return nil
8383
}
8484
}
85+
86+
// EmulatePty returns a functional option that sets PtyCallback on the server
87+
// to emulate a "fake" PTY by using PtyWriter.
88+
func EmulatePty() Option {
89+
return func(srv *Server) error {
90+
srv.PtyCallback = func(_ Context, pty Pty) bool {
91+
pty.emulate = true
92+
return true
93+
}
94+
return nil
95+
}
96+
}
97+
98+
// AllocatePty returns a functional option that sets PtyCallback on the server
99+
// to allocate a PTY for sessions that request it.
100+
func AllocatePty() Option {
101+
return func(srv *Server) error {
102+
srv.PtyCallback = func(_ Context, pty Pty) bool {
103+
err := pty.allocate()
104+
return err == nil
105+
}
106+
return nil
107+
}
108+
}

pty.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package ssh
33
import (
44
"bytes"
55
"io"
6+
7+
"github.com/aymanbagabas/go-pty"
68
)
79

810
// NewPtyWriter creates a writer that handles when the session has a active
@@ -55,3 +57,23 @@ func (rw readWriterDelegate) Read(p []byte) (n int, err error) {
5557
func (rw readWriterDelegate) Write(p []byte) (n int, err error) {
5658
return rw.w.Write(p)
5759
}
60+
61+
// allocate is a helper function to allocate a pty session.
62+
// It returns a Pty object that can be used to run a command on a tty.
63+
// If this fails, use NewPtyReadWriter to _emulate_ a tty.
64+
func (p *Pty) allocate() error {
65+
tty, err := pty.New()
66+
if err != nil {
67+
return err
68+
}
69+
70+
p.Pty = tty
71+
72+
if err := pty.ApplyTerminalModes(int(tty.Fd()),
73+
p.Window.Width, p.Window.Height, p.Modes,
74+
); err != nil {
75+
return err
76+
}
77+
78+
return nil
79+
}

session.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"io"
7+
"log"
78
"net"
89
"sync"
910

@@ -128,14 +129,14 @@ type session struct {
128129
}
129130

130131
func (sess *session) Stderr() io.ReadWriter {
131-
if sess.pty != nil {
132+
if sess.pty != nil && sess.pty.emulate {
132133
return NewPtyReadWriter(sess.Channel.Stderr())
133134
}
134135
return sess.Channel.Stderr()
135136
}
136137

137138
func (sess *session) Write(p []byte) (int, error) {
138-
if sess.pty != nil {
139+
if sess.pty != nil && sess.pty.emulate {
139140
return NewPtyWriter(sess.Channel).Write(p)
140141
}
141142
return sess.Channel.Write(p)
@@ -331,6 +332,22 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
331332
continue
332333
}
333334
}
335+
if err := ptyReq.allocate(); err != nil {
336+
log.Printf("pty allocation failed: %v", err)
337+
req.Reply(false, nil)
338+
continue
339+
}
340+
if ptyReq.Pty != nil {
341+
log.Printf("pty allocated: %s", ptyReq.Pty.Name())
342+
sess.env = append(sess.env,
343+
fmt.Sprint("TERM=", ptyReq.Term),
344+
fmt.Sprintf("SSH_TTY=%s", ptyReq.Pty.Name()),
345+
)
346+
// input to pty
347+
go io.Copy(ptyReq.Pty, sess) // nolint: errcheck
348+
// pty to output
349+
go io.Copy(sess, ptyReq.Pty) // nolint: errcheck
350+
}
334351
sess.pty = &ptyReq
335352
sess.winch = make(chan Window, 1)
336353
sess.winch <- ptyReq.Window
@@ -348,6 +365,9 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
348365
if ok {
349366
sess.pty.Window = win
350367
sess.winch <- win
368+
if sess.pty.Pty != nil {
369+
sess.pty.Pty.Resize(win.Width, win.Height)
370+
}
351371
}
352372
req.Reply(ok, nil)
353373
case agentRequestType:

ssh.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/subtle"
55
"net"
66

7+
"github.com/aymanbagabas/go-pty"
78
gossh "golang.org/x/crypto/ssh"
89
)
910

@@ -91,6 +92,14 @@ type Window struct {
9192

9293
// Pty represents a PTY request and configuration.
9394
type Pty struct {
95+
// If this is true, the server will emulate a "fake" Pty by using
96+
// PtyWriter.
97+
emulate bool
98+
99+
// Pty is the actual pty allocated. It is nil if the request did not
100+
// allocated a pty.
101+
pty.Pty
102+
94103
// Term is the TERM environment variable value.
95104
Term string
96105

0 commit comments

Comments
 (0)