]> Cypherpunks repositories - gostls13.git/commitdiff
exp/ssh: remove duplicated terminal code.
authorAdam Langley <agl@golang.org>
Sun, 15 Jan 2012 14:59:06 +0000 (09:59 -0500)
committerAdam Langley <agl@golang.org>
Sun, 15 Jan 2012 14:59:06 +0000 (09:59 -0500)
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
src/pkg/exp/ssh/doc.go
src/pkg/exp/ssh/server_shell.go [deleted file]
src/pkg/exp/ssh/server_terminal.go [new file with mode: 0644]
src/pkg/exp/ssh/session_test.go

index 1b75d5aacda11c9e1fd0d0795c7da286542cdd46..9c044443276e62a99660a4a9dc15f475750a4eeb 100644 (file)
@@ -13,7 +13,7 @@ GOFILES=\
        common.go\
        messages.go\
        server.go\
-       server_shell.go\
+       server_terminal.go\
        session.go\
        tcpip.go\
        transport.go\
index 4ea402c5d2d2158acb0ba6a90f049f42f3453977..e7deb5ec168831488a3c9600c77cf1b86ad02c41 100644 (file)
@@ -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 (file)
index 5243d0e..0000000
+++ /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 (file)
index 0000000..708a915
--- /dev/null
@@ -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")
+}
index 2882620b0ba3c89c54953429c8e36cb45ca512e9..4a3d22bee04f905be3cdf581f7a8a12955892c8b 100644 (file)
@@ -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)
 }