Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for terminal opcodes #210

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
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
18 changes: 7 additions & 11 deletions session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
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