Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allocate real pty #8

Merged
merged 11 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ var (
// ContextKeyPublicKey is a context key for use with Contexts in this package.
// The associated value will be of type PublicKey.
ContextKeyPublicKey = &contextKey{"public-key"}

// ContextKeySession is a context key for use with Contexts in this package.
// The associated value will be of type Session.
ContextKeySession = &contextKey{"session"}
)

// Context is a package specific context interface. It exposes connection
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ go 1.17

require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/creack/pty v1.1.21
github.com/u-root/u-root v0.11.0
aymanbagabas marked this conversation as resolved.
Show resolved Hide resolved
golang.org/x/crypto v0.17.0
)

require golang.org/x/sys v0.15.0 // indirect
require golang.org/x/sys v0.16.0
338 changes: 337 additions & 1 deletion go.sum

Large diffs are not rendered by default.

30 changes: 29 additions & 1 deletion options.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func HostKeyPEM(bytes []byte) Option {
// denying PTY requests.
func NoPty() Option {
return func(srv *Server) error {
srv.PtyCallback = func(ctx Context, pty Pty) bool {
srv.PtyCallback = func(Context, Pty) bool {
return false
}
return nil
Expand All @@ -82,3 +82,31 @@ func WrapConn(fn ConnCallback) Option {
return nil
}
}

var contextKeyEmulatePty = &contextKey{"emulate-pty"}

func emulatePtyHandler(ctx Context, _ Session, _ Pty) (func() error, error) {
ctx.SetValue(contextKeyEmulatePty, true)
return func() error { return nil }, nil
}

// EmulatePty returns a functional option that fakes a PTY. It uses PtyWriter
// underneath.
func EmulatePty() Option {
return func(s *Server) error {
s.PtyHandler = emulatePtyHandler
return nil
}
}

// AllocatePty returns a functional option that allocates a PTY. Implementers
// who wish to use an actual PTY should use this along with the platform
// specific PTY implementation defined in pty_*.go.
func AllocatePty() Option {
return func(s *Server) error {
s.PtyHandler = func(_ Context, s Session, pty Pty) (func() error, error) {
return s.(*session).ptyAllocate(pty.Term, pty.Window, pty.Modes)
}
return nil
}
}
4 changes: 4 additions & 0 deletions pty.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package ssh

import (
"bytes"
"errors"
"io"
)

// ErrUnsupported is returned when the platform does not support PTY.
var ErrUnsupported = errors.New("pty unsupported")

// NewPtyWriter creates a writer that handles when the session has a active
// PTY, replacing the \n with \r\n.
func NewPtyWriter(w io.Writer) io.Writer {
Expand Down
31 changes: 31 additions & 0 deletions pty_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris
// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris

// TODO: support Windows
package ssh

import (
"golang.org/x/crypto/ssh"
)

type impl struct{}

func (i *impl) Read(p []byte) (n int, err error) {
return 0, ErrUnsupported
}

func (i *impl) Write(p []byte) (n int, err error) {
return 0, ErrUnsupported
}

func (i *impl) Resize(w int, h int) error {
return ErrUnsupported
}

func (i *impl) Close() error {
return nil
}

func newPty(Context, string, Window, ssh.TerminalModes) (impl, error) {
return impl{}, ErrUnsupported
}
181 changes: 181 additions & 0 deletions pty_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris

package ssh

import (
"fmt"
"os"

"github.com/creack/pty"
"github.com/u-root/u-root/pkg/termios"
"golang.org/x/crypto/ssh"
"golang.org/x/sys/unix"
)

type impl struct {
// Master is the master PTY file descriptor.
Master *os.File

// Slave is the slave PTY file descriptor.
Slave *os.File

// Name is the name of the slave PTY.
Name string
}

// Read implements ptyInterface.
func (i *impl) Read(p []byte) (n int, err error) {
return i.Master.Read(p)
}

// Write implements ptyInterface.
func (i *impl) Write(p []byte) (n int, err error) {
return i.Master.Write(p)
}

func (i *impl) Close() error {
if err := i.Master.Close(); err != nil {
return err
}
return i.Slave.Close()
}

func (i *impl) Resize(w int, h int) (rErr error) {
conn, err := i.Master.SyscallConn()
if err != nil {
return err
}

return conn.Control(func(fd uintptr) {
rErr = termios.SetWinSize(fd, &termios.Winsize{
Winsize: unix.Winsize{
Row: uint16(h),
Col: uint16(w),
},
})
})
}

func newPty(_ Context, _ string, win Window, modes ssh.TerminalModes) (_ impl, rErr error) {
ptm, pts, err := pty.Open()
if err != nil {
return impl{}, err
}

conn, err := ptm.SyscallConn()
if err != nil {
return impl{}, err
}

if err := conn.Control(func(fd uintptr) {
rErr = applyTerminalModesToFd(fd, win.Width, win.Height, modes)
}); err != nil {
return impl{}, err
}

return impl{Master: ptm, Slave: pts, Name: pts.Name()}, rErr
}

func applyTerminalModesToFd(fd uintptr, width int, height int, modes ssh.TerminalModes) error {
// Get the current TTY configuration.
tios, err := termios.GTTY(int(fd))
if err != nil {
return fmt.Errorf("GTTY: %w", err)
}

// Apply the modes from the SSH request.
tios.Row = height
tios.Col = width

for c, v := range modes {
if c == ssh.TTY_OP_ISPEED {
tios.Ispeed = int(v)
continue
}
if c == ssh.TTY_OP_OSPEED {
tios.Ospeed = int(v)
continue
}
k, ok := terminalModeFlagNames[c]
if !ok {
continue
}
if _, ok := tios.CC[k]; ok {
tios.CC[k] = uint8(v)
continue
}
if _, ok := tios.Opts[k]; ok {
tios.Opts[k] = v > 0
continue
}
}

// Save the new TTY configuration.
if _, err := tios.STTY(int(fd)); err != nil {
return fmt.Errorf("STTY: %w", err)
}

return nil
}

// terminalModeFlagNames maps the SSH terminal mode flags to mnemonic
// names used by the termios package.
var terminalModeFlagNames = map[uint8]string{
ssh.VINTR: "intr",
ssh.VQUIT: "quit",
ssh.VERASE: "erase",
ssh.VKILL: "kill",
ssh.VEOF: "eof",
ssh.VEOL: "eol",
ssh.VEOL2: "eol2",
ssh.VSTART: "start",
ssh.VSTOP: "stop",
ssh.VSUSP: "susp",
ssh.VDSUSP: "dsusp",
ssh.VREPRINT: "rprnt",
ssh.VWERASE: "werase",
ssh.VLNEXT: "lnext",
ssh.VFLUSH: "flush",
ssh.VSWTCH: "swtch",
ssh.VSTATUS: "status",
ssh.VDISCARD: "discard",
ssh.IGNPAR: "ignpar",
ssh.PARMRK: "parmrk",
ssh.INPCK: "inpck",
ssh.ISTRIP: "istrip",
ssh.INLCR: "inlcr",
ssh.IGNCR: "igncr",
ssh.ICRNL: "icrnl",
ssh.IUCLC: "iuclc",
ssh.IXON: "ixon",
ssh.IXANY: "ixany",
ssh.IXOFF: "ixoff",
ssh.IMAXBEL: "imaxbel",
ssh.IUTF8: "iutf8",
ssh.ISIG: "isig",
ssh.ICANON: "icanon",
ssh.XCASE: "xcase",
ssh.ECHO: "echo",
ssh.ECHOE: "echoe",
ssh.ECHOK: "echok",
ssh.ECHONL: "echonl",
ssh.NOFLSH: "noflsh",
ssh.TOSTOP: "tostop",
ssh.IEXTEN: "iexten",
ssh.ECHOCTL: "echoctl",
ssh.ECHOKE: "echoke",
ssh.PENDIN: "pendin",
ssh.OPOST: "opost",
ssh.OLCUC: "olcuc",
ssh.ONLCR: "onlcr",
ssh.OCRNL: "ocrnl",
ssh.ONOCR: "onocr",
ssh.ONLRET: "onlret",
ssh.CS7: "cs7",
ssh.CS8: "cs8",
ssh.PARENB: "parenb",
ssh.PARODD: "parodd",
ssh.TTY_OP_ISPEED: "tty_op_ispeed",
ssh.TTY_OP_OSPEED: "tty_op_ospeed",
}
16 changes: 10 additions & 6 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ type Server struct {
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
PtyCallback PtyCallback // callback for allocating and allowing PTY sessions, ssh.EmulatePtyCallback if nil
PtyHandler PtyHandler // pty allocation handler, ssh.emulatePtyHandler if nil
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil
Expand Down Expand Up @@ -116,8 +117,8 @@ func (srv *Server) ensureHandlers() {
}

func (srv *Server) config(ctx Context) *gossh.ServerConfig {
srv.mu.RLock()
defer srv.mu.RUnlock()
srv.mu.Lock()
defer srv.mu.Unlock()

var config *gossh.ServerConfig
if srv.ServerConfigCallback == nil {
Expand All @@ -131,6 +132,9 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil && srv.KeyboardInteractiveHandler == nil {
config.NoClientAuth = true
}
if srv.PtyHandler == nil {
srv.PtyHandler = emulatePtyHandler
}
if srv.Version != "" {
config.ServerVersion = "SSH-2.0-" + srv.Version
}
Expand Down Expand Up @@ -304,7 +308,7 @@ func (srv *Server) HandleConn(newConn net.Conn) {

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

//srv.mu.Lock()
//defer srv.mu.Unlock()
// srv.mu.Lock()
// defer srv.mu.Unlock()

return option(srv)
}
Expand Down
Loading