Skip to content

Commit

Permalink
add support for terminal opcodes
Browse files Browse the repository at this point in the history
Updates tailscale/tailscale#4146

Signed-off-by: Maisem Ali <maisem@tailscale.com>
  • Loading branch information
maisem authored and aymanbagabas committed Jul 31, 2023
1 parent cf1ec7e commit 123ec27
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 30 deletions.
2 changes: 1 addition & 1 deletion session.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,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
Expand Down
34 changes: 31 additions & 3 deletions ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,44 @@ type ServerConfigCallback func(ctx Context) *gossh.ServerConfig
type ConnectionFailedCallback func(conn net.Conn, err error)

// Window represents the size of a PTY window.
//
// From 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.
//
// Note: golang.org/x/crypto/ssh currently (2022-03-12) doesn't have a
// definition for opcode 42 "iutf8" which was introduced in https://datatracker.ietf.org/doc/html/rfc8160.
Modes gossh.TerminalModes
}

// Serve accepts incoming SSH connections on the listener l, creating a new
Expand Down
125 changes: 99 additions & 26 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,61 +16,134 @@ 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) {
// From https://datatracker.ietf.org/doc/html/rfc4254
// 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) {
// From https://datatracker.ietf.org/doc/html/rfc4254
// 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) {
// 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
}
Expand Down

0 comments on commit 123ec27

Please sign in to comment.