// 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") }