Skip to content

Commit 72503be

Browse files
committed
feat: allocate real pty
This adds a new PtyHandler to handle allocating PTYs and storing them in a platform specific field in `Pty`. This PR is backward-compatible, it defaults to EmulatePty handler that sets the `emulatePty` field in context and uses `PtyWriter` to preserve the current behavor.
1 parent e11ae27 commit 72503be

File tree

10 files changed

+678
-11
lines changed

10 files changed

+678
-11
lines changed

context.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ var (
5656
// ContextKeyPublicKey is a context key for use with Contexts in this package.
5757
// The associated value will be of type PublicKey.
5858
ContextKeyPublicKey = &contextKey{"public-key"}
59+
60+
// ContextKeySession is a context key for use with Contexts in this package.
61+
// The associated value will be of type Session.
62+
ContextKeySession = &contextKey{"session"}
5963
)
6064

6165
// Context is a package specific context interface. It exposes connection

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ go 1.17
44

55
require (
66
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
7+
github.com/creack/pty v1.1.21
8+
github.com/u-root/u-root v0.11.0
79
golang.org/x/crypto v0.17.0
810
)
911

10-
require golang.org/x/sys v0.15.0 // indirect
12+
require golang.org/x/sys v0.16.0

go.sum

Lines changed: 337 additions & 1 deletion
Large diffs are not rendered by default.

options.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func HostKeyPEM(bytes []byte) Option {
6868
// denying PTY requests.
6969
func NoPty() Option {
7070
return func(srv *Server) error {
71-
srv.PtyCallback = func(ctx Context, pty Pty) bool {
71+
srv.PtyCallback = func(Context, Pty) bool {
7272
return false
7373
}
7474
return nil
@@ -82,3 +82,22 @@ func WrapConn(fn ConnCallback) Option {
8282
return nil
8383
}
8484
}
85+
86+
var contextKeyEmulatePty = &contextKey{"emulate-pty"}
87+
88+
// EmulatePty returns true if the session is set to emulate a PTY.
89+
func EmulatePty(ctx Context, _ Session, _ Pty) (func() error, error) {
90+
ctx.SetValue(contextKeyEmulatePty, true)
91+
return func() error { return nil }, nil
92+
}
93+
94+
// AllocatePty returns a functional option that sets PtyCallback on the server
95+
// to allocate a PTY for sessions that request it.
96+
func AllocatePty(_ Context, s Session, pty Pty) (func() error, error) {
97+
sess, ok := s.(*session)
98+
if !ok {
99+
return nil, nil
100+
}
101+
102+
return sess.ptyAllocate(pty.Term, pty.Window, pty.Modes)
103+
}

pty.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ package ssh
22

33
import (
44
"bytes"
5+
"errors"
56
"io"
67
)
78

9+
// ErrUnsupported is returned when the platform does not support PTY.
10+
var ErrUnsupported = errors.New("pty unsupported")
11+
812
// NewPtyWriter creates a writer that handles when the session has a active
913
// PTY, replacing the \n with \r\n.
1014
func NewPtyWriter(w io.Writer) io.Writer {

pty_other.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris
2+
// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris
3+
4+
// TODO: support Windows
5+
package ssh
6+
7+
import (
8+
"golang.org/x/crypto/ssh"
9+
)
10+
11+
type impl struct{}
12+
13+
func (i *impl) Read(p []byte) (n int, err error) {
14+
return 0, ErrUnsupported
15+
}
16+
17+
func (i *impl) Write(p []byte) (n int, err error) {
18+
return 0, ErrUnsupported
19+
}
20+
21+
func (i *impl) Resize(w int, h int) error {
22+
return ErrUnsupported
23+
}
24+
25+
func (i *impl) Close() error {
26+
return nil
27+
}
28+
29+
func newPty(Context, string, Window, ssh.TerminalModes) (impl, error) {
30+
return impl{}, ErrUnsupported
31+
}

pty_unix.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
2+
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
3+
4+
package ssh
5+
6+
import (
7+
"fmt"
8+
"os"
9+
10+
"github.com/creack/pty"
11+
"github.com/u-root/u-root/pkg/termios"
12+
"golang.org/x/crypto/ssh"
13+
"golang.org/x/sys/unix"
14+
)
15+
16+
type impl struct {
17+
// Master is the master PTY file descriptor.
18+
Master *os.File
19+
20+
// Slave is the slave PTY file descriptor.
21+
Slave *os.File
22+
23+
// Name is the name of the slave PTY.
24+
Name string
25+
}
26+
27+
// Read implements ptyInterface.
28+
func (i *impl) Read(p []byte) (n int, err error) {
29+
return i.Master.Read(p)
30+
}
31+
32+
// Write implements ptyInterface.
33+
func (i *impl) Write(p []byte) (n int, err error) {
34+
return i.Master.Write(p)
35+
}
36+
37+
func (i *impl) Close() error {
38+
if err := i.Master.Close(); err != nil {
39+
return err
40+
}
41+
return i.Slave.Close()
42+
}
43+
44+
func (i *impl) Resize(w int, h int) (rErr error) {
45+
conn, err := i.Master.SyscallConn()
46+
if err != nil {
47+
return err
48+
}
49+
50+
return conn.Control(func(fd uintptr) {
51+
rErr = termios.SetWinSize(fd, &termios.Winsize{
52+
Winsize: unix.Winsize{
53+
Row: uint16(h),
54+
Col: uint16(w),
55+
},
56+
})
57+
})
58+
}
59+
60+
func newPty(_ Context, _ string, win Window, modes ssh.TerminalModes) (_ impl, rErr error) {
61+
ptm, pts, err := pty.Open()
62+
if err != nil {
63+
return impl{}, err
64+
}
65+
66+
conn, err := ptm.SyscallConn()
67+
if err != nil {
68+
return impl{}, err
69+
}
70+
71+
if err := conn.Control(func(fd uintptr) {
72+
rErr = applyTerminalModesToFd(fd, win.Width, win.Height, modes)
73+
}); err != nil {
74+
return impl{}, err
75+
}
76+
77+
return impl{Master: ptm, Slave: pts, Name: pts.Name()}, rErr
78+
}
79+
80+
func applyTerminalModesToFd(fd uintptr, width int, height int, modes ssh.TerminalModes) error {
81+
// Get the current TTY configuration.
82+
tios, err := termios.GTTY(int(fd))
83+
if err != nil {
84+
return fmt.Errorf("GTTY: %w", err)
85+
}
86+
87+
// Apply the modes from the SSH request.
88+
tios.Row = height
89+
tios.Col = width
90+
91+
for c, v := range modes {
92+
if c == ssh.TTY_OP_ISPEED {
93+
tios.Ispeed = int(v)
94+
continue
95+
}
96+
if c == ssh.TTY_OP_OSPEED {
97+
tios.Ospeed = int(v)
98+
continue
99+
}
100+
k, ok := terminalModeFlagNames[c]
101+
if !ok {
102+
continue
103+
}
104+
if _, ok := tios.CC[k]; ok {
105+
tios.CC[k] = uint8(v)
106+
continue
107+
}
108+
if _, ok := tios.Opts[k]; ok {
109+
tios.Opts[k] = v > 0
110+
continue
111+
}
112+
}
113+
114+
// Save the new TTY configuration.
115+
if _, err := tios.STTY(int(fd)); err != nil {
116+
return fmt.Errorf("STTY: %w", err)
117+
}
118+
119+
return nil
120+
}
121+
122+
// terminalModeFlagNames maps the SSH terminal mode flags to mnemonic
123+
// names used by the termios package.
124+
var terminalModeFlagNames = map[uint8]string{
125+
ssh.VINTR: "intr",
126+
ssh.VQUIT: "quit",
127+
ssh.VERASE: "erase",
128+
ssh.VKILL: "kill",
129+
ssh.VEOF: "eof",
130+
ssh.VEOL: "eol",
131+
ssh.VEOL2: "eol2",
132+
ssh.VSTART: "start",
133+
ssh.VSTOP: "stop",
134+
ssh.VSUSP: "susp",
135+
ssh.VDSUSP: "dsusp",
136+
ssh.VREPRINT: "rprnt",
137+
ssh.VWERASE: "werase",
138+
ssh.VLNEXT: "lnext",
139+
ssh.VFLUSH: "flush",
140+
ssh.VSWTCH: "swtch",
141+
ssh.VSTATUS: "status",
142+
ssh.VDISCARD: "discard",
143+
ssh.IGNPAR: "ignpar",
144+
ssh.PARMRK: "parmrk",
145+
ssh.INPCK: "inpck",
146+
ssh.ISTRIP: "istrip",
147+
ssh.INLCR: "inlcr",
148+
ssh.IGNCR: "igncr",
149+
ssh.ICRNL: "icrnl",
150+
ssh.IUCLC: "iuclc",
151+
ssh.IXON: "ixon",
152+
ssh.IXANY: "ixany",
153+
ssh.IXOFF: "ixoff",
154+
ssh.IMAXBEL: "imaxbel",
155+
ssh.IUTF8: "iutf8",
156+
ssh.ISIG: "isig",
157+
ssh.ICANON: "icanon",
158+
ssh.XCASE: "xcase",
159+
ssh.ECHO: "echo",
160+
ssh.ECHOE: "echoe",
161+
ssh.ECHOK: "echok",
162+
ssh.ECHONL: "echonl",
163+
ssh.NOFLSH: "noflsh",
164+
ssh.TOSTOP: "tostop",
165+
ssh.IEXTEN: "iexten",
166+
ssh.ECHOCTL: "echoctl",
167+
ssh.ECHOKE: "echoke",
168+
ssh.PENDIN: "pendin",
169+
ssh.OPOST: "opost",
170+
ssh.OLCUC: "olcuc",
171+
ssh.ONLCR: "onlcr",
172+
ssh.OCRNL: "ocrnl",
173+
ssh.ONOCR: "onocr",
174+
ssh.ONLRET: "onlret",
175+
ssh.CS7: "cs7",
176+
ssh.CS8: "cs8",
177+
ssh.PARENB: "parenb",
178+
ssh.PARODD: "parodd",
179+
ssh.TTY_OP_ISPEED: "tty_op_ispeed",
180+
ssh.TTY_OP_OSPEED: "tty_op_ospeed",
181+
}

server.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ type Server struct {
4343
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
4444
PasswordHandler PasswordHandler // password authentication handler
4545
PublicKeyHandler PublicKeyHandler // public key authentication handler
46-
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
46+
PtyCallback PtyCallback // callback for allocating and allowing PTY sessions, ssh.EmulatePtyCallback if nil
47+
PtyHandler PtyHandler // pty allocation handler, ssh.EmulatePty if nil
4748
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
4849
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
4950
ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil
@@ -131,6 +132,9 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
131132
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil && srv.KeyboardInteractiveHandler == nil {
132133
config.NoClientAuth = true
133134
}
135+
if srv.PtyHandler == nil {
136+
srv.PtyHandler = EmulatePty
137+
}
134138
if srv.Version != "" {
135139
config.ServerVersion = "SSH-2.0-" + srv.Version
136140
}
@@ -304,7 +308,7 @@ func (srv *Server) HandleConn(newConn net.Conn) {
304308

305309
ctx.SetValue(ContextKeyConn, sshConn)
306310
applyConnMetadata(ctx, sshConn)
307-
//go gossh.DiscardRequests(reqs)
311+
// go gossh.DiscardRequests(reqs)
308312
go srv.handleRequests(ctx, reqs)
309313
for ch := range chans {
310314
handler := srv.ChannelHandlers[ch.ChannelType()]
@@ -381,8 +385,8 @@ func (srv *Server) SetOption(option Option) error {
381385
// internal method. We can't actually lock here because if something calls
382386
// (as an example) AddHostKey, it will deadlock.
383387

384-
//srv.mu.Lock()
385-
//defer srv.mu.Unlock()
388+
// srv.mu.Lock()
389+
// defer srv.mu.Unlock()
386390

387391
return option(srv)
388392
}

0 commit comments

Comments
 (0)