Skip to content

Commit 517184b

Browse files
Maisem Alimafredri
Maisem Ali
authored andcommitted
add support for terminal opcodes
Updates tailscale/tailscale#4146 Signed-off-by: Maisem Ali <maisem@tailscale.com>
1 parent 777ab34 commit 517184b

File tree

3 files changed

+131
-30
lines changed

3 files changed

+131
-30
lines changed

session.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
346346
req.Reply(false, nil)
347347
continue
348348
}
349-
win, ok := parseWinchRequest(req.Payload)
349+
win, _, ok := parseWindow(req.Payload)
350350
if ok {
351351
sess.pty.Window = win
352352
sess.winch <- win

ssh.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,44 @@ type ServerConfigCallback func(ctx Context) *gossh.ServerConfig
6969
type ConnectionFailedCallback func(conn net.Conn, err error)
7070

7171
// Window represents the size of a PTY window.
72+
//
73+
// From https://datatracker.ietf.org/doc/html/rfc4254#section-6.2
74+
//
75+
// Zero dimension parameters MUST be ignored. The character/row dimensions
76+
// override the pixel dimensions (when nonzero). Pixel dimensions refer
77+
// to the drawable area of the window.
7278
type Window struct {
73-
Width int
79+
// Width is the number of columns.
80+
// It overrides WidthPixels.
81+
Width int
82+
// Height is the number of rows.
83+
// It overrides HeightPixels.
7484
Height int
85+
86+
// WidthPixels is the drawable width of the window, in pixels.
87+
WidthPixels int
88+
// HeightPixels is the drawable height of the window, in pixels.
89+
HeightPixels int
7590
}
7691

7792
// Pty represents a PTY request and configuration.
7893
type Pty struct {
79-
Term string
94+
// Term is the TERM environment variable value.
95+
Term string
96+
97+
// Window is the Window sent as part of the pty-req.
8098
Window Window
81-
// HELP WANTED: terminal modes!
99+
100+
// Modes represent a mapping of Terminal Mode opcode to value as it was
101+
// requested by the client as part of the pty-req. These are outlined as
102+
// part of https://datatracker.ietf.org/doc/html/rfc4254#section-8.
103+
//
104+
// The opcodes are defined as constants in golang.org/x/crypto/ssh (VINTR,VQUIT,etc.).
105+
// Boolean opcodes have values 0 or 1.
106+
//
107+
// Note: golang.org/x/crypto/ssh currently (2022-03-12) doesn't have a
108+
// definition for opcode 42 "iutf8" which was introduced in https://datatracker.ietf.org/doc/html/rfc8160.
109+
Modes gossh.TerminalModes
82110
}
83111

84112
// Serve accepts incoming SSH connections on the listener l, creating a new

util.go

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,61 +16,134 @@ func generateSigner() (ssh.Signer, error) {
1616
return ssh.NewSignerFromKey(key)
1717
}
1818

19-
func parsePtyRequest(s []byte) (pty Pty, ok bool) {
20-
term, s, ok := parseString(s)
19+
func parsePtyRequest(payload []byte) (pty Pty, ok bool) {
20+
// From https://datatracker.ietf.org/doc/html/rfc4254
21+
// 6.2. Requesting a Pseudo-Terminal
22+
// A pseudo-terminal can be allocated for the session by sending the
23+
// following message.
24+
// byte SSH_MSG_CHANNEL_REQUEST
25+
// uint32 recipient channel
26+
// string "pty-req"
27+
// boolean want_reply
28+
// string TERM environment variable value (e.g., vt100)
29+
// uint32 terminal width, characters (e.g., 80)
30+
// uint32 terminal height, rows (e.g., 24)
31+
// uint32 terminal width, pixels (e.g., 640)
32+
// uint32 terminal height, pixels (e.g., 480)
33+
// string encoded terminal modes
34+
35+
// The payload starts from the TERM variable.
36+
term, rem, ok := parseString(payload)
2137
if !ok {
2238
return
2339
}
24-
width32, s, ok := parseUint32(s)
40+
win, rem, ok := parseWindow(rem)
2541
if !ok {
2642
return
2743
}
28-
height32, _, ok := parseUint32(s)
44+
modes, ok := parseTerminalModes(rem)
2945
if !ok {
3046
return
3147
}
3248
pty = Pty{
33-
Term: term,
34-
Window: Window{
35-
Width: int(width32),
36-
Height: int(height32),
37-
},
49+
Term: term,
50+
Window: win,
51+
Modes: modes,
3852
}
3953
return
4054
}
4155

42-
func parseWinchRequest(s []byte) (win Window, ok bool) {
43-
width32, s, ok := parseUint32(s)
44-
if width32 < 1 {
45-
ok = false
56+
func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) {
57+
// From https://datatracker.ietf.org/doc/html/rfc4254
58+
// 8. Encoding of Terminal Modes
59+
//
60+
// All 'encoded terminal modes' (as passed in a pty request) are encoded
61+
// into a byte stream. It is intended that the coding be portable
62+
// across different environments. The stream consists of opcode-
63+
// argument pairs wherein the opcode is a byte value. Opcodes 1 to 159
64+
// have a single uint32 argument. Opcodes 160 to 255 are not yet
65+
// defined, and cause parsing to stop (they should only be used after
66+
// any other data). The stream is terminated by opcode TTY_OP_END
67+
// (0x00).
68+
//
69+
// The client SHOULD put any modes it knows about in the stream, and the
70+
// server MAY ignore any modes it does not know about. This allows some
71+
// degree of machine-independence, at least between systems that use a
72+
// POSIX-like tty interface. The protocol can support other systems as
73+
// well, but the client may need to fill reasonable values for a number
74+
// of parameters so the server pty gets set to a reasonable mode (the
75+
// server leaves all unspecified mode bits in their default values, and
76+
// only some combinations make sense).
77+
_, rem, ok := parseUint32(in)
78+
if !ok {
79+
return
80+
}
81+
const ttyOpEnd = 0
82+
for len(rem) > 0 {
83+
if modes == nil {
84+
modes = make(ssh.TerminalModes)
85+
}
86+
code := uint8(rem[0])
87+
rem = rem[1:]
88+
if code == ttyOpEnd || code > 160 {
89+
break
90+
}
91+
var val uint32
92+
val, rem, ok = parseUint32(rem)
93+
if !ok {
94+
return
95+
}
96+
modes[code] = val
97+
}
98+
ok = true
99+
return
100+
}
101+
102+
func parseWindow(s []byte) (win Window, rem []byte, ok bool) {
103+
// 6.7. Window Dimension Change Message
104+
// When the window (terminal) size changes on the client side, it MAY
105+
// send a message to the other side to inform it of the new dimensions.
106+
107+
// byte SSH_MSG_CHANNEL_REQUEST
108+
// uint32 recipient channel
109+
// string "window-change"
110+
// boolean FALSE
111+
// uint32 terminal width, columns
112+
// uint32 terminal height, rows
113+
// uint32 terminal width, pixels
114+
// uint32 terminal height, pixels
115+
wCols, rem, ok := parseUint32(s)
116+
if !ok {
117+
return
46118
}
119+
hRows, rem, ok := parseUint32(rem)
47120
if !ok {
48121
return
49122
}
50-
height32, _, ok := parseUint32(s)
51-
if height32 < 1 {
52-
ok = false
123+
wPixels, rem, ok := parseUint32(rem)
124+
if !ok {
125+
return
53126
}
127+
hPixels, rem, ok := parseUint32(rem)
54128
if !ok {
55129
return
56130
}
57131
win = Window{
58-
Width: int(width32),
59-
Height: int(height32),
132+
Width: int(wCols),
133+
Height: int(hRows),
134+
WidthPixels: int(wPixels),
135+
HeightPixels: int(hPixels),
60136
}
61137
return
62138
}
63139

64-
func parseString(in []byte) (out string, rest []byte, ok bool) {
65-
if len(in) < 4 {
66-
return
67-
}
68-
length := binary.BigEndian.Uint32(in)
69-
if uint32(len(in)) < 4+length {
140+
func parseString(in []byte) (out string, rem []byte, ok bool) {
141+
length, rem, ok := parseUint32(in)
142+
if uint32(len(rem)) < length || !ok {
143+
ok = false
70144
return
71145
}
72-
out = string(in[4 : 4+length])
73-
rest = in[4+length:]
146+
out, rem = string(rem[:length]), rem[length:]
74147
ok = true
75148
return
76149
}

0 commit comments

Comments
 (0)