aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam Langley <agl@golang.org>2012-01-15 09:59:06 -0500
committerAdam Langley <agl@golang.org>2012-01-15 09:59:06 -0500
commitdd47a0a2ca62e8c7f275545d3aa810d3b875550f (patch)
tree62e9271477dcbf00492262ed8454a8ab80e383b3
parent4dda23a1d0f793f53661dd973e29c793e67aa9aa (diff)
downloadgo-dd47a0a2ca62e8c7f275545d3aa810d3b875550f.tar.gz
go-dd47a0a2ca62e8c7f275545d3aa810d3b875550f.zip
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
-rw-r--r--src/pkg/exp/ssh/Makefile2
-rw-r--r--src/pkg/exp/ssh/doc.go24
-rw-r--r--src/pkg/exp/ssh/server_shell.go398
-rw-r--r--src/pkg/exp/ssh/server_terminal.go81
-rw-r--r--src/pkg/exp/ssh/session_test.go23
5 files changed, 112 insertions, 416 deletions
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)
}