From dd47a0a2ca62e8c7f275545d3aa810d3b875550f Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Sun, 15 Jan 2012 09:59:06 -0500 Subject: [PATCH] exp/ssh: remove duplicated terminal code. The terminal code in exp/terminal was forked from the code in exp/ssh. This change removes the duplicated code from exp/ssh in favour of using exp/terminal. R=rsc CC=golang-dev https://golang.org/cl/5375064 --- src/pkg/exp/ssh/Makefile | 2 +- src/pkg/exp/ssh/doc.go | 24 +- src/pkg/exp/ssh/server_shell.go | 398 ----------------------------- src/pkg/exp/ssh/server_terminal.go | 81 ++++++ src/pkg/exp/ssh/session_test.go | 23 +- 5 files changed, 112 insertions(+), 416 deletions(-) delete mode 100644 src/pkg/exp/ssh/server_shell.go create mode 100644 src/pkg/exp/ssh/server_terminal.go diff --git a/src/pkg/exp/ssh/Makefile b/src/pkg/exp/ssh/Makefile index 1b75d5aacd..9c04444327 100644 --- a/src/pkg/exp/ssh/Makefile +++ b/src/pkg/exp/ssh/Makefile @@ -13,7 +13,7 @@ GOFILES=\ common.go\ messages.go\ server.go\ - server_shell.go\ + server_terminal.go\ session.go\ tcpip.go\ transport.go\ diff --git a/src/pkg/exp/ssh/doc.go b/src/pkg/exp/ssh/doc.go index 4ea402c5d2..e7deb5ec16 100644 --- a/src/pkg/exp/ssh/doc.go +++ b/src/pkg/exp/ssh/doc.go @@ -14,7 +14,7 @@ others. An SSH server is represented by a ServerConfig, which holds certificate details and handles authentication of ServerConns. - config := new(ServerConfig) + config := new(ssh.ServerConfig) config.PubKeyCallback = pubKeyAuth config.PasswordCallback = passwordAuth @@ -34,8 +34,7 @@ Once a ServerConfig has been configured, connections can be accepted. if err != nil { panic("failed to accept incoming connection") } - err = sConn.Handshake(conn) - if err != nil { + if err := sConn.Handshake(conn); err != nil { panic("failed to handshake") } @@ -60,16 +59,20 @@ the case of a shell, the type is "session" and ServerShell may be used to present a simple terminal interface. if channel.ChannelType() != "session" { - c.Reject(UnknownChannelType, "unknown channel type") + channel.Reject(UnknownChannelType, "unknown channel type") return } channel.Accept() - shell := NewServerShell(channel, "> ") + term := terminal.NewTerminal(channel, "> ") + serverTerm := &ssh.ServerTerminal{ + Term: term, + Channel: channel, + } go func() { defer channel.Close() for { - line, err := shell.ReadLine() + line, err := serverTerm.ReadLine() if err != nil { break } @@ -97,7 +100,8 @@ ClientAuth via the Auth field in ClientConfig. } } -An SSH client is represented with a ClientConn. +An SSH client is represented with a ClientConn. Currently only the "password" +authentication method is supported. config := &ClientConfig{ User: "username", @@ -105,12 +109,12 @@ An SSH client is represented with a ClientConn. } client, err := Dial("yourserver.com:22", config) -Each ClientConn can support multiple interactive sessions, represented by a Session. +Each ClientConn can support multiple interactive sessions, represented by a Session. session, err := client.NewSession() -Once a Session is created, you can execute a single command on the remote side -using the Run method. +Once a Session is created, you can execute a single command on the remote side +using the Exec method. b := bytes.NewBuffer() session.Stdin = b diff --git a/src/pkg/exp/ssh/server_shell.go b/src/pkg/exp/ssh/server_shell.go deleted file mode 100644 index 5243d0ee7f..0000000000 --- a/src/pkg/exp/ssh/server_shell.go +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package ssh - -import "io" - -// ServerShell contains the state for running a VT100 terminal that is capable -// of reading lines of input. -type ServerShell struct { - c Channel - prompt string - - // line is the current line being entered. - line []byte - // pos is the logical position of the cursor in line - pos int - - // cursorX contains the current X value of the cursor where the left - // edge is 0. cursorY contains the row number where the first row of - // the current line is 0. - cursorX, cursorY int - // maxLine is the greatest value of cursorY so far. - maxLine int - - termWidth, termHeight int - - // outBuf contains the terminal data to be sent. - outBuf []byte - // remainder contains the remainder of any partial key sequences after - // a read. It aliases into inBuf. - remainder []byte - inBuf [256]byte -} - -// NewServerShell runs a VT100 terminal on the given channel. prompt is a -// string that is written at the start of each input line. For example: "> ". -func NewServerShell(c Channel, prompt string) *ServerShell { - return &ServerShell{ - c: c, - prompt: prompt, - termWidth: 80, - termHeight: 24, - } -} - -const ( - keyCtrlD = 4 - keyEnter = '\r' - keyEscape = 27 - keyBackspace = 127 - keyUnknown = 256 + iota - keyUp - keyDown - keyLeft - keyRight - keyAltLeft - keyAltRight -) - -// bytesToKey tries to parse a key sequence from b. If successful, it returns -// the key and the remainder of the input. Otherwise it returns -1. -func bytesToKey(b []byte) (int, []byte) { - if len(b) == 0 { - return -1, nil - } - - if b[0] != keyEscape { - return int(b[0]), b[1:] - } - - if len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { - switch b[2] { - case 'A': - return keyUp, b[3:] - case 'B': - return keyDown, b[3:] - case 'C': - return keyRight, b[3:] - case 'D': - return keyLeft, b[3:] - } - } - - if len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { - switch b[5] { - case 'C': - return keyAltRight, b[6:] - case 'D': - return keyAltLeft, b[6:] - } - } - - // If we get here then we have a key that we don't recognise, or a - // partial sequence. It's not clear how one should find the end of a - // sequence without knowing them all, but it seems that [a-zA-Z] only - // appears at the end of a sequence. - for i, c := range b[0:] { - if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' { - return keyUnknown, b[i+1:] - } - } - - return -1, b -} - -// queue appends data to the end of ss.outBuf -func (ss *ServerShell) queue(data []byte) { - if len(ss.outBuf)+len(data) > cap(ss.outBuf) { - newOutBuf := make([]byte, len(ss.outBuf), 2*(len(ss.outBuf)+len(data))) - copy(newOutBuf, ss.outBuf) - ss.outBuf = newOutBuf - } - - oldLen := len(ss.outBuf) - ss.outBuf = ss.outBuf[:len(ss.outBuf)+len(data)] - copy(ss.outBuf[oldLen:], data) -} - -var eraseUnderCursor = []byte{' ', keyEscape, '[', 'D'} - -func isPrintable(key int) bool { - return key >= 32 && key < 127 -} - -// moveCursorToPos appends data to ss.outBuf which will move the cursor to the -// given, logical position in the text. -func (ss *ServerShell) moveCursorToPos(pos int) { - x := len(ss.prompt) + pos - y := x / ss.termWidth - x = x % ss.termWidth - - up := 0 - if y < ss.cursorY { - up = ss.cursorY - y - } - - down := 0 - if y > ss.cursorY { - down = y - ss.cursorY - } - - left := 0 - if x < ss.cursorX { - left = ss.cursorX - x - } - - right := 0 - if x > ss.cursorX { - right = x - ss.cursorX - } - - movement := make([]byte, 3*(up+down+left+right)) - m := movement - for i := 0; i < up; i++ { - m[0] = keyEscape - m[1] = '[' - m[2] = 'A' - m = m[3:] - } - for i := 0; i < down; i++ { - m[0] = keyEscape - m[1] = '[' - m[2] = 'B' - m = m[3:] - } - for i := 0; i < left; i++ { - m[0] = keyEscape - m[1] = '[' - m[2] = 'D' - m = m[3:] - } - for i := 0; i < right; i++ { - m[0] = keyEscape - m[1] = '[' - m[2] = 'C' - m = m[3:] - } - - ss.cursorX = x - ss.cursorY = y - ss.queue(movement) -} - -const maxLineLength = 4096 - -// handleKey processes the given key and, optionally, returns a line of text -// that the user has entered. -func (ss *ServerShell) handleKey(key int) (line string, ok bool) { - switch key { - case keyBackspace: - if ss.pos == 0 { - return - } - ss.pos-- - - copy(ss.line[ss.pos:], ss.line[1+ss.pos:]) - ss.line = ss.line[:len(ss.line)-1] - ss.writeLine(ss.line[ss.pos:]) - ss.moveCursorToPos(ss.pos) - ss.queue(eraseUnderCursor) - case keyAltLeft: - // move left by a word. - if ss.pos == 0 { - return - } - ss.pos-- - for ss.pos > 0 { - if ss.line[ss.pos] != ' ' { - break - } - ss.pos-- - } - for ss.pos > 0 { - if ss.line[ss.pos] == ' ' { - ss.pos++ - break - } - ss.pos-- - } - ss.moveCursorToPos(ss.pos) - case keyAltRight: - // move right by a word. - for ss.pos < len(ss.line) { - if ss.line[ss.pos] == ' ' { - break - } - ss.pos++ - } - for ss.pos < len(ss.line) { - if ss.line[ss.pos] != ' ' { - break - } - ss.pos++ - } - ss.moveCursorToPos(ss.pos) - case keyLeft: - if ss.pos == 0 { - return - } - ss.pos-- - ss.moveCursorToPos(ss.pos) - case keyRight: - if ss.pos == len(ss.line) { - return - } - ss.pos++ - ss.moveCursorToPos(ss.pos) - case keyEnter: - ss.moveCursorToPos(len(ss.line)) - ss.queue([]byte("\r\n")) - line = string(ss.line) - ok = true - ss.line = ss.line[:0] - ss.pos = 0 - ss.cursorX = 0 - ss.cursorY = 0 - ss.maxLine = 0 - default: - if !isPrintable(key) { - return - } - if len(ss.line) == maxLineLength { - return - } - if len(ss.line) == cap(ss.line) { - newLine := make([]byte, len(ss.line), 2*(1+len(ss.line))) - copy(newLine, ss.line) - ss.line = newLine - } - ss.line = ss.line[:len(ss.line)+1] - copy(ss.line[ss.pos+1:], ss.line[ss.pos:]) - ss.line[ss.pos] = byte(key) - ss.writeLine(ss.line[ss.pos:]) - ss.pos++ - ss.moveCursorToPos(ss.pos) - } - return -} - -func (ss *ServerShell) writeLine(line []byte) { - for len(line) != 0 { - if ss.cursorX == ss.termWidth { - ss.queue([]byte("\r\n")) - ss.cursorX = 0 - ss.cursorY++ - if ss.cursorY > ss.maxLine { - ss.maxLine = ss.cursorY - } - } - - remainingOnLine := ss.termWidth - ss.cursorX - todo := len(line) - if todo > remainingOnLine { - todo = remainingOnLine - } - ss.queue(line[:todo]) - ss.cursorX += todo - line = line[todo:] - } -} - -// parsePtyRequest parses the payload of the pty-req message and extracts the -// dimensions of the terminal. See RFC 4254, section 6.2. -func parsePtyRequest(s []byte) (width, height int, ok bool) { - _, s, ok = parseString(s) - if !ok { - return - } - width32, s, ok := parseUint32(s) - if !ok { - return - } - height32, _, ok := parseUint32(s) - width = int(width32) - height = int(height32) - if width < 1 { - ok = false - } - if height < 1 { - ok = false - } - return -} - -func (ss *ServerShell) Write(buf []byte) (n int, err error) { - return ss.c.Write(buf) -} - -// ReadLine returns a line of input from the terminal. -func (ss *ServerShell) ReadLine() (line string, err error) { - ss.writeLine([]byte(ss.prompt)) - ss.c.Write(ss.outBuf) - ss.outBuf = ss.outBuf[:0] - - for { - // ss.remainder is a slice at the beginning of ss.inBuf - // containing a partial key sequence - readBuf := ss.inBuf[len(ss.remainder):] - var n int - n, err = ss.c.Read(readBuf) - if err == nil { - ss.remainder = ss.inBuf[:n+len(ss.remainder)] - rest := ss.remainder - lineOk := false - for !lineOk { - var key int - key, rest = bytesToKey(rest) - if key < 0 { - break - } - if key == keyCtrlD { - return "", io.EOF - } - line, lineOk = ss.handleKey(key) - } - if len(rest) > 0 { - n := copy(ss.inBuf[:], rest) - ss.remainder = ss.inBuf[:n] - } else { - ss.remainder = nil - } - ss.c.Write(ss.outBuf) - ss.outBuf = ss.outBuf[:0] - if lineOk { - return - } - continue - } - - if req, ok := err.(ChannelRequest); ok { - ok := false - switch req.Request { - case "pty-req": - ss.termWidth, ss.termHeight, ok = parsePtyRequest(req.Payload) - if !ok { - ss.termWidth = 80 - ss.termHeight = 24 - } - case "shell": - ok = true - if len(req.Payload) > 0 { - // We don't accept any commands, only the default shell. - ok = false - } - case "env": - ok = true - } - if req.WantReply { - ss.c.AckRequest(ok) - } - } else { - return "", err - } - } - panic("unreachable") -} diff --git a/src/pkg/exp/ssh/server_terminal.go b/src/pkg/exp/ssh/server_terminal.go new file mode 100644 index 0000000000..708a9159ec --- /dev/null +++ b/src/pkg/exp/ssh/server_terminal.go @@ -0,0 +1,81 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ssh + +// A Terminal is capable of parsing and generating virtual terminal +// data from an SSH client. +type Terminal interface { + ReadLine() (line string, err error) + SetSize(x, y int) + Write([]byte) (int, error) +} + +// ServerTerminal contains the state for running a terminal that is capable of +// reading lines of input. +type ServerTerminal struct { + Term Terminal + Channel Channel +} + +// parsePtyRequest parses the payload of the pty-req message and extracts the +// dimensions of the terminal. See RFC 4254, section 6.2. +func parsePtyRequest(s []byte) (width, height int, ok bool) { + _, s, ok = parseString(s) + if !ok { + return + } + width32, s, ok := parseUint32(s) + if !ok { + return + } + height32, _, ok := parseUint32(s) + width = int(width32) + height = int(height32) + if width < 1 { + ok = false + } + if height < 1 { + ok = false + } + return +} + +func (ss *ServerTerminal) Write(buf []byte) (n int, err error) { + return ss.Term.Write(buf) +} + +// ReadLine returns a line of input from the terminal. +func (ss *ServerTerminal) ReadLine() (line string, err error) { + for { + if line, err = ss.Term.ReadLine(); err == nil { + return + } + + req, ok := err.(ChannelRequest) + if !ok { + return + } + + ok = false + switch req.Request { + case "pty-req": + var width, height int + width, height, ok = parsePtyRequest(req.Payload) + ss.Term.SetSize(width, height) + case "shell": + ok = true + if len(req.Payload) > 0 { + // We don't accept any commands, only the default shell. + ok = false + } + case "env": + ok = true + } + if req.WantReply { + ss.Channel.AckRequest(ok) + } + } + panic("unreachable") +} diff --git a/src/pkg/exp/ssh/session_test.go b/src/pkg/exp/ssh/session_test.go index 2882620b0b..4a3d22bee0 100644 --- a/src/pkg/exp/ssh/session_test.go +++ b/src/pkg/exp/ssh/session_test.go @@ -8,6 +8,7 @@ package ssh import ( "bytes" + "exp/terminal" "io" "testing" ) @@ -290,24 +291,32 @@ type exitSignalMsg struct { Lang string } +func newServerShell(ch *channel, prompt string) *ServerTerminal { + term := terminal.NewTerminal(ch, prompt) + return &ServerTerminal{ + Term: term, + Channel: ch, + } +} + func exitStatusZeroHandler(ch *channel) { defer ch.Close() // this string is returned to stdout - shell := NewServerShell(ch, "> ") + shell := newServerShell(ch, "> ") shell.ReadLine() sendStatus(0, ch) } func exitStatusNonZeroHandler(ch *channel) { defer ch.Close() - shell := NewServerShell(ch, "> ") + shell := newServerShell(ch, "> ") shell.ReadLine() sendStatus(15, ch) } func exitSignalAndStatusHandler(ch *channel) { defer ch.Close() - shell := NewServerShell(ch, "> ") + shell := newServerShell(ch, "> ") shell.ReadLine() sendStatus(15, ch) sendSignal("TERM", ch) @@ -315,28 +324,28 @@ func exitSignalAndStatusHandler(ch *channel) { func exitSignalHandler(ch *channel) { defer ch.Close() - shell := NewServerShell(ch, "> ") + shell := newServerShell(ch, "> ") shell.ReadLine() sendSignal("TERM", ch) } func exitSignalUnknownHandler(ch *channel) { defer ch.Close() - shell := NewServerShell(ch, "> ") + shell := newServerShell(ch, "> ") shell.ReadLine() sendSignal("SYS", ch) } func exitWithoutSignalOrStatus(ch *channel) { defer ch.Close() - shell := NewServerShell(ch, "> ") + shell := newServerShell(ch, "> ") shell.ReadLine() } func shellHandler(ch *channel) { defer ch.Close() // this string is returned to stdout - shell := NewServerShell(ch, "golang") + shell := newServerShell(ch, "golang") shell.ReadLine() sendStatus(0, ch) }