diff --git a/go.mod b/go.mod index 6d83084..eee9a4f 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/gliderlabs/ssh +module github.com/tailscale/ssh go 1.12 diff --git a/session.go b/session.go index fbe5692..a7a9a3e 100644 --- a/session.go +++ b/session.go @@ -83,6 +83,13 @@ type Session interface { // the request handling loop. Registering nil will unregister the channel. // During the time that no channel is registered, breaks are ignored. Break(c chan<- bool) + + // DisablePTYEmulation disables the session's default minimal PTY emulation. + // If you're setting the pty's termios settings from the Pty request, use + // this method to avoid corruption. + // Currently (2022-03-12) the only emulation implemented is NL-to-CRNL translation (`\n`=>`\r\n`). + // A call of DisablePTYEmulation must precede any call to Write. + DisablePTYEmulation() } // maxSigBufSize is how many signals will be buffered @@ -110,26 +117,31 @@ func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.Ne type session struct { sync.Mutex gossh.Channel - conn *gossh.ServerConn - handler Handler - subsystemHandlers map[string]SubsystemHandler - handled bool - exited bool - pty *Pty - winch chan Window - env []string - ptyCb PtyCallback - sessReqCb SessionRequestCallback - rawCmd string - subsystem string - ctx Context - sigCh chan<- Signal - sigBuf []Signal - breakCh chan<- bool + conn *gossh.ServerConn + handler Handler + subsystemHandlers map[string]SubsystemHandler + handled bool + exited bool + pty *Pty + winch chan Window + env []string + ptyCb PtyCallback + sessReqCb SessionRequestCallback + rawCmd string + subsystem string + ctx Context + sigCh chan<- Signal + sigBuf []Signal + breakCh chan<- bool + disablePtyEmulation bool +} + +func (sess *session) DisablePTYEmulation() { + sess.disablePtyEmulation = true } func (sess *session) Write(p []byte) (n int, err error) { - if sess.pty != nil { + if sess.pty != nil && !sess.disablePtyEmulation { m := len(p) // normalize \n to \r\n when pty is accepted. // this is a hardcoded shortcut since we don't support terminal modes. @@ -347,7 +359,7 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) { req.Reply(false, nil) continue } - win, ok := parseWinchRequest(req.Payload) + win, _, ok := parseWindow(req.Payload) if ok { sess.pty.Window = win sess.winch <- win diff --git a/ssh.go b/ssh.go index fbeb150..8bb02a3 100644 --- a/ssh.go +++ b/ssh.go @@ -69,16 +69,41 @@ type ServerConfigCallback func(ctx Context) *gossh.ServerConfig type ConnectionFailedCallback func(conn net.Conn, err error) // Window represents the size of a PTY window. +// +// See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2 +// +// Zero dimension parameters MUST be ignored. The character/row dimensions +// override the pixel dimensions (when nonzero). Pixel dimensions refer +// to the drawable area of the window. type Window struct { - Width int + // Width is the number of columns. + // It overrides WidthPixels. + Width int + // Height is the number of rows. + // It overrides HeightPixels. Height int + + // WidthPixels is the drawable width of the window, in pixels. + WidthPixels int + // HeightPixels is the drawable height of the window, in pixels. + HeightPixels int } // Pty represents a PTY request and configuration. type Pty struct { - Term string + // Term is the TERM environment variable value. + Term string + + // Window is the Window sent as part of the pty-req. Window Window - // HELP WANTED: terminal modes! + + // Modes represent a mapping of Terminal Mode opcode to value as it was + // requested by the client as part of the pty-req. These are outlined as + // part of https://datatracker.ietf.org/doc/html/rfc4254#section-8. + // + // The opcodes are defined as constants in golang.org/x/crypto/ssh (VINTR,VQUIT,etc.). + // Boolean opcodes have values 0 or 1. + Modes gossh.TerminalModes } // Serve accepts incoming SSH connections on the listener l, creating a new diff --git a/util.go b/util.go index 015a44e..3bee06d 100644 --- a/util.go +++ b/util.go @@ -16,61 +16,135 @@ func generateSigner() (ssh.Signer, error) { return ssh.NewSignerFromKey(key) } -func parsePtyRequest(s []byte) (pty Pty, ok bool) { - term, s, ok := parseString(s) +func parsePtyRequest(payload []byte) (pty Pty, ok bool) { + // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2 + // 6.2. Requesting a Pseudo-Terminal + // A pseudo-terminal can be allocated for the session by sending the + // following message. + // byte SSH_MSG_CHANNEL_REQUEST + // uint32 recipient channel + // string "pty-req" + // boolean want_reply + // string TERM environment variable value (e.g., vt100) + // uint32 terminal width, characters (e.g., 80) + // uint32 terminal height, rows (e.g., 24) + // uint32 terminal width, pixels (e.g., 640) + // uint32 terminal height, pixels (e.g., 480) + // string encoded terminal modes + + // The payload starts from the TERM variable. + term, rem, ok := parseString(payload) if !ok { return } - width32, s, ok := parseUint32(s) + win, rem, ok := parseWindow(rem) if !ok { return } - height32, _, ok := parseUint32(s) + modes, ok := parseTerminalModes(rem) if !ok { return } pty = Pty{ - Term: term, - Window: Window{ - Width: int(width32), - Height: int(height32), - }, + Term: term, + Window: win, + Modes: modes, } return } -func parseWinchRequest(s []byte) (win Window, ok bool) { - width32, s, ok := parseUint32(s) - if width32 < 1 { - ok = false +func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) { + // See https://datatracker.ietf.org/doc/html/rfc4254#section-8 + // 8. Encoding of Terminal Modes + // + // All 'encoded terminal modes' (as passed in a pty request) are encoded + // into a byte stream. It is intended that the coding be portable + // across different environments. The stream consists of opcode- + // argument pairs wherein the opcode is a byte value. Opcodes 1 to 159 + // have a single uint32 argument. Opcodes 160 to 255 are not yet + // defined, and cause parsing to stop (they should only be used after + // any other data). The stream is terminated by opcode TTY_OP_END + // (0x00). + // + // The client SHOULD put any modes it knows about in the stream, and the + // server MAY ignore any modes it does not know about. This allows some + // degree of machine-independence, at least between systems that use a + // POSIX-like tty interface. The protocol can support other systems as + // well, but the client may need to fill reasonable values for a number + // of parameters so the server pty gets set to a reasonable mode (the + // server leaves all unspecified mode bits in their default values, and + // only some combinations make sense). + _, rem, ok := parseUint32(in) + if !ok { + return + } + const ttyOpEnd = 0 + for len(rem) > 0 { + if modes == nil { + modes = make(ssh.TerminalModes) + } + code := uint8(rem[0]) + rem = rem[1:] + if code == ttyOpEnd || code > 160 { + break + } + var val uint32 + val, rem, ok = parseUint32(rem) + if !ok { + return + } + modes[code] = val + } + ok = true + return +} + +func parseWindow(s []byte) (win Window, rem []byte, ok bool) { + // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.7 + // 6.7. Window Dimension Change Message + // When the window (terminal) size changes on the client side, it MAY + // send a message to the other side to inform it of the new dimensions. + + // byte SSH_MSG_CHANNEL_REQUEST + // uint32 recipient channel + // string "window-change" + // boolean FALSE + // uint32 terminal width, columns + // uint32 terminal height, rows + // uint32 terminal width, pixels + // uint32 terminal height, pixels + wCols, rem, ok := parseUint32(s) + if !ok { + return } + hRows, rem, ok := parseUint32(rem) if !ok { return } - height32, _, ok := parseUint32(s) - if height32 < 1 { - ok = false + wPixels, rem, ok := parseUint32(rem) + if !ok { + return } + hPixels, rem, ok := parseUint32(rem) if !ok { return } win = Window{ - Width: int(width32), - Height: int(height32), + Width: int(wCols), + Height: int(hRows), + WidthPixels: int(wPixels), + HeightPixels: int(hPixels), } return } -func parseString(in []byte) (out string, rest []byte, ok bool) { - if len(in) < 4 { - return - } - length := binary.BigEndian.Uint32(in) - if uint32(len(in)) < 4+length { +func parseString(in []byte) (out string, rem []byte, ok bool) { + length, rem, ok := parseUint32(in) + if uint32(len(rem)) < length || !ok { + ok = false return } - out = string(in[4 : 4+length]) - rest = in[4+length:] + out, rem = string(rem[:length]), rem[length:] ok = true return }