Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
10 changes: 8 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
module github.com/charmbracelet/ssh

go 1.17
go 1.21

toolchain go1.21.5

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
golang.org/x/crypto v0.17.0
)

require golang.org/x/sys v0.15.0 // indirect
require golang.org/x/sys v0.16.0

require github.com/charmbracelet/x/term v0.0.0-20240109162103-b354873f6f2c
47 changes: 10 additions & 37 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,43 +1,16 @@
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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
github.com/charmbracelet/x/term v0.0.0-20240109162103-b354873f6f2c h1:pm2npO09x7mp7OES8BmqqEuVCEXyhj1XEjrJlKJpN1A=
github.com/charmbracelet/x/term v0.0.0-20240109162103-b354873f6f2c/go.mod h1:viW49uh1UJ6F1f2UglwW4tZo7eJn5wZXEq4PKt3873Q=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0=
github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90/go.mod h1:lYt+LVfZBBwDZ3+PHk4k/c/TnKOkjJXiJO73E32Mmpc=
github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8=
github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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
}
}
14 changes: 14 additions & 0 deletions pty.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ package ssh

import (
"bytes"
"errors"
"io"
"os/exec"
)

// 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 Expand Up @@ -55,3 +60,12 @@ 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)
}

// Start starts a *exec.Cmd attached to the Session. If a PTY is allocated,
// it will use that for I/O.
// On Windows, the process execution lifecycle is not managed by Go and has to
// be managed manually. This means that c.Wait() won't work.
// See https://github.com/charmbracelet/x/blob/main/term/conpty/conpty_windows.go#L155
func (p *Pty) Start(c *exec.Cmd) error {
return p.start(c)
}
36 changes: 36 additions & 0 deletions pty_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris && !windows
// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris,!windows

package ssh

import (
"os/exec"

"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 (*impl) start(*exec.Cmd) error {
return ErrUnsupported
}

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

package ssh

import (
"fmt"
"os"
"os/exec"
"syscall"

"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 (i *impl) start(c *exec.Cmd) error {
c.Stdin, c.Stdout, c.Stderr := i.Slave, i.Slave, i.Slave
if c.SysProcAttr == nil {
c.SysProcAttr = &syscall.SysProcAttr{}
}
}

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",
}
Loading