From 05438ffd46cc7fed3066af47f7b1fc43e8c4653b Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Sat, 12 Mar 2022 17:12:09 -0800 Subject: [PATCH] add support for terminal opcodes Updates tailscale/tailscale#4146 Signed-off-by: Maisem Ali Co-Authored-By: Ayman Bagabas --- session.go | 2 +- session_test.go | 18 +++---- ssh.go | 34 +++++++++++-- util.go | 125 ++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 138 insertions(+), 41 deletions(-) diff --git a/session.go b/session.go index 3a3ad70..fb47581 100644 --- a/session.go +++ b/session.go @@ -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 diff --git a/session_test.go b/session_test.go index c6ce617..9d19654 100644 --- a/session_test.go +++ b/session_test.go @@ -230,9 +230,9 @@ func TestPty(t *testing.T) { func TestPtyResize(t *testing.T) { t.Parallel() - winch0 := Window{40, 80} - winch1 := Window{80, 160} - winch2 := Window{20, 40} + winch0 := Window{40, 80, 320, 640} + winch1 := Window{80, 160, 640, 1280} + winch2 := Window{20, 40, 160, 320} winches := make(chan Window) done := make(chan bool) session, _, cleanup := newTestSession(t, &Server{ @@ -263,20 +263,16 @@ func TestPtyResize(t *testing.T) { t.Fatalf("expected window %#v but got %#v", winch0, gotWinch) } // winch1 - winchMsg := struct{ w, h uint32 }{uint32(winch1.Width), uint32(winch1.Height)} - ok, err := session.SendRequest("window-change", true, gossh.Marshal(&winchMsg)) - if err == nil && !ok { - t.Fatalf("unexpected error or bad reply on send request") + if err := session.WindowChange(winch1.Height, winch1.Width); err != nil { + t.Fatalf("expected nil but got %v", err) } gotWinch = <-winches if gotWinch != winch1 { t.Fatalf("expected window %#v but got %#v", winch1, gotWinch) } // winch2 - winchMsg = struct{ w, h uint32 }{uint32(winch2.Width), uint32(winch2.Height)} - ok, err = session.SendRequest("window-change", true, gossh.Marshal(&winchMsg)) - if err == nil && !ok { - t.Fatalf("unexpected error or bad reply on send request") + if err := session.WindowChange(winch2.Height, winch2.Width); err != nil { + t.Fatalf("expected nil but got %v", err) } gotWinch = <-winches if gotWinch != winch2 { diff --git a/ssh.go b/ssh.go index fbeb150..7dc76b3 100644 --- a/ssh.go +++ b/ssh.go @@ -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 diff --git a/util.go b/util.go index 015a44e..a5e6699 100644 --- a/util.go +++ b/util.go @@ -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 }