From 34e6ae60502cff0e9480526bc59c657acb525680 Mon Sep 17 00:00:00 2001 From: David Symonds Date: Fri, 30 Mar 2012 15:27:01 +1100 Subject: [PATCH 01/58] go.crypto: add exp/terminal as code.google.com/p/go.crypto/ssh/terminal. This removes the sole "exp/foo" import in the Go subrepos. A separate CL will remove exp/terminal from the standard Go repository. R=golang-dev, dave, r CC=golang-dev https://golang.org/cl/5966045 --- terminal.go | 520 +++++++++++++++++++++++++++++++++++++++++++++++ terminal_test.go | 110 ++++++++++ util.go | 115 +++++++++++ 3 files changed, 745 insertions(+) create mode 100644 terminal.go create mode 100644 terminal_test.go create mode 100644 util.go diff --git a/terminal.go b/terminal.go new file mode 100644 index 0000000..c1ed0c0 --- /dev/null +++ b/terminal.go @@ -0,0 +1,520 @@ +// 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 terminal + +import ( + "io" + "sync" +) + +// EscapeCodes contains escape sequences that can be written to the terminal in +// order to achieve different styles of text. +type EscapeCodes struct { + // Foreground colors + Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte + + // Reset all attributes + Reset []byte +} + +var vt100EscapeCodes = EscapeCodes{ + Black: []byte{keyEscape, '[', '3', '0', 'm'}, + Red: []byte{keyEscape, '[', '3', '1', 'm'}, + Green: []byte{keyEscape, '[', '3', '2', 'm'}, + Yellow: []byte{keyEscape, '[', '3', '3', 'm'}, + Blue: []byte{keyEscape, '[', '3', '4', 'm'}, + Magenta: []byte{keyEscape, '[', '3', '5', 'm'}, + Cyan: []byte{keyEscape, '[', '3', '6', 'm'}, + White: []byte{keyEscape, '[', '3', '7', 'm'}, + + Reset: []byte{keyEscape, '[', '0', 'm'}, +} + +// Terminal contains the state for running a VT100 terminal that is capable of +// reading lines of input. +type Terminal struct { + // AutoCompleteCallback, if non-null, is called for each keypress + // with the full input line and the current position of the cursor. + // If it returns a nil newLine, the key press is processed normally. + // Otherwise it returns a replacement line and the new cursor position. + AutoCompleteCallback func(line []byte, pos, key int) (newLine []byte, newPos int) + + // Escape contains a pointer to the escape codes for this terminal. + // It's always a valid pointer, although the escape codes themselves + // may be empty if the terminal doesn't support them. + Escape *EscapeCodes + + // lock protects the terminal and the state in this object from + // concurrent processing of a key press and a Write() call. + lock sync.Mutex + + c io.ReadWriter + prompt string + + // line is the current line being entered. + line []byte + // pos is the logical position of the cursor in line + pos int + // echo is true if local echo is enabled + echo bool + + // 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 +} + +// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is +// a local terminal, that terminal must first have been put into raw mode. +// prompt is a string that is written at the start of each input line (i.e. +// "> "). +func NewTerminal(c io.ReadWriter, prompt string) *Terminal { + return &Terminal{ + Escape: &vt100EscapeCodes, + c: c, + prompt: prompt, + termWidth: 80, + termHeight: 24, + echo: true, + } +} + +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 t.outBuf +func (t *Terminal) queue(data []byte) { + t.outBuf = append(t.outBuf, data...) +} + +var eraseUnderCursor = []byte{' ', keyEscape, '[', 'D'} +var space = []byte{' '} + +func isPrintable(key int) bool { + return key >= 32 && key < 127 +} + +// moveCursorToPos appends data to t.outBuf which will move the cursor to the +// given, logical position in the text. +func (t *Terminal) moveCursorToPos(pos int) { + if !t.echo { + return + } + + x := len(t.prompt) + pos + y := x / t.termWidth + x = x % t.termWidth + + up := 0 + if y < t.cursorY { + up = t.cursorY - y + } + + down := 0 + if y > t.cursorY { + down = y - t.cursorY + } + + left := 0 + if x < t.cursorX { + left = t.cursorX - x + } + + right := 0 + if x > t.cursorX { + right = x - t.cursorX + } + + t.cursorX = x + t.cursorY = y + t.move(up, down, left, right) +} + +func (t *Terminal) move(up, down, left, right int) { + 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:] + } + + t.queue(movement) +} + +func (t *Terminal) clearLineToRight() { + op := []byte{keyEscape, '[', 'K'} + t.queue(op) +} + +const maxLineLength = 4096 + +// handleKey processes the given key and, optionally, returns a line of text +// that the user has entered. +func (t *Terminal) handleKey(key int) (line string, ok bool) { + switch key { + case keyBackspace: + if t.pos == 0 { + return + } + t.pos-- + t.moveCursorToPos(t.pos) + + copy(t.line[t.pos:], t.line[1+t.pos:]) + t.line = t.line[:len(t.line)-1] + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.queue(eraseUnderCursor) + t.moveCursorToPos(t.pos) + case keyAltLeft: + // move left by a word. + if t.pos == 0 { + return + } + t.pos-- + for t.pos > 0 { + if t.line[t.pos] != ' ' { + break + } + t.pos-- + } + for t.pos > 0 { + if t.line[t.pos] == ' ' { + t.pos++ + break + } + t.pos-- + } + t.moveCursorToPos(t.pos) + case keyAltRight: + // move right by a word. + for t.pos < len(t.line) { + if t.line[t.pos] == ' ' { + break + } + t.pos++ + } + for t.pos < len(t.line) { + if t.line[t.pos] != ' ' { + break + } + t.pos++ + } + t.moveCursorToPos(t.pos) + case keyLeft: + if t.pos == 0 { + return + } + t.pos-- + t.moveCursorToPos(t.pos) + case keyRight: + if t.pos == len(t.line) { + return + } + t.pos++ + t.moveCursorToPos(t.pos) + case keyEnter: + t.moveCursorToPos(len(t.line)) + t.queue([]byte("\r\n")) + line = string(t.line) + ok = true + t.line = t.line[:0] + t.pos = 0 + t.cursorX = 0 + t.cursorY = 0 + t.maxLine = 0 + default: + if t.AutoCompleteCallback != nil { + t.lock.Unlock() + newLine, newPos := t.AutoCompleteCallback(t.line, t.pos, key) + t.lock.Lock() + + if newLine != nil { + if t.echo { + t.moveCursorToPos(0) + t.writeLine(newLine) + for i := len(newLine); i < len(t.line); i++ { + t.writeLine(space) + } + t.moveCursorToPos(newPos) + } + t.line = newLine + t.pos = newPos + return + } + } + if !isPrintable(key) { + return + } + if len(t.line) == maxLineLength { + return + } + if len(t.line) == cap(t.line) { + newLine := make([]byte, len(t.line), 2*(1+len(t.line))) + copy(newLine, t.line) + t.line = newLine + } + t.line = t.line[:len(t.line)+1] + copy(t.line[t.pos+1:], t.line[t.pos:]) + t.line[t.pos] = byte(key) + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.pos++ + t.moveCursorToPos(t.pos) + } + return +} + +func (t *Terminal) writeLine(line []byte) { + for len(line) != 0 { + remainingOnLine := t.termWidth - t.cursorX + todo := len(line) + if todo > remainingOnLine { + todo = remainingOnLine + } + t.queue(line[:todo]) + t.cursorX += todo + line = line[todo:] + + if t.cursorX == t.termWidth { + t.cursorX = 0 + t.cursorY++ + if t.cursorY > t.maxLine { + t.maxLine = t.cursorY + } + } + } +} + +func (t *Terminal) Write(buf []byte) (n int, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + if t.cursorX == 0 && t.cursorY == 0 { + // This is the easy case: there's nothing on the screen that we + // have to move out of the way. + return t.c.Write(buf) + } + + // We have a prompt and possibly user input on the screen. We + // have to clear it first. + t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */) + t.cursorX = 0 + t.clearLineToRight() + + for t.cursorY > 0 { + t.move(1 /* up */, 0, 0, 0) + t.cursorY-- + t.clearLineToRight() + } + + if _, err = t.c.Write(t.outBuf); err != nil { + return + } + t.outBuf = t.outBuf[:0] + + if n, err = t.c.Write(buf); err != nil { + return + } + + t.queue([]byte(t.prompt)) + chars := len(t.prompt) + if t.echo { + t.queue(t.line) + chars += len(t.line) + } + t.cursorX = chars % t.termWidth + t.cursorY = chars / t.termWidth + t.moveCursorToPos(t.pos) + + if _, err = t.c.Write(t.outBuf); err != nil { + return + } + t.outBuf = t.outBuf[:0] + return +} + +// ReadPassword temporarily changes the prompt and reads a password, without +// echo, from the terminal. +func (t *Terminal) ReadPassword(prompt string) (line string, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + oldPrompt := t.prompt + t.prompt = prompt + t.echo = false + + line, err = t.readLine() + + t.prompt = oldPrompt + t.echo = true + + return +} + +// ReadLine returns a line of input from the terminal. +func (t *Terminal) ReadLine() (line string, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + return t.readLine() +} + +func (t *Terminal) readLine() (line string, err error) { + // t.lock must be held at this point + + if t.cursorX == 0 && t.cursorY == 0 { + t.writeLine([]byte(t.prompt)) + t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + } + + for { + rest := t.remainder + lineOk := false + for !lineOk { + var key int + key, rest = bytesToKey(rest) + if key < 0 { + break + } + if key == keyCtrlD { + return "", io.EOF + } + line, lineOk = t.handleKey(key) + } + if len(rest) > 0 { + n := copy(t.inBuf[:], rest) + t.remainder = t.inBuf[:n] + } else { + t.remainder = nil + } + t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + if lineOk { + return + } + + // t.remainder is a slice at the beginning of t.inBuf + // containing a partial key sequence + readBuf := t.inBuf[len(t.remainder):] + var n int + + t.lock.Unlock() + n, err = t.c.Read(readBuf) + t.lock.Lock() + + if err != nil { + return + } + + t.remainder = t.inBuf[:n+len(t.remainder)] + } + panic("unreachable") +} + +// SetPrompt sets the prompt to be used when reading subsequent lines. +func (t *Terminal) SetPrompt(prompt string) { + t.lock.Lock() + defer t.lock.Unlock() + + t.prompt = prompt +} + +func (t *Terminal) SetSize(width, height int) { + t.lock.Lock() + defer t.lock.Unlock() + + t.termWidth, t.termHeight = width, height +} diff --git a/terminal_test.go b/terminal_test.go new file mode 100644 index 0000000..a219721 --- /dev/null +++ b/terminal_test.go @@ -0,0 +1,110 @@ +// 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 terminal + +import ( + "io" + "testing" +) + +type MockTerminal struct { + toSend []byte + bytesPerRead int + received []byte +} + +func (c *MockTerminal) Read(data []byte) (n int, err error) { + n = len(data) + if n == 0 { + return + } + if n > len(c.toSend) { + n = len(c.toSend) + } + if n == 0 { + return 0, io.EOF + } + if c.bytesPerRead > 0 && n > c.bytesPerRead { + n = c.bytesPerRead + } + copy(data, c.toSend[:n]) + c.toSend = c.toSend[n:] + return +} + +func (c *MockTerminal) Write(data []byte) (n int, err error) { + c.received = append(c.received, data...) + return len(data), nil +} + +func TestClose(t *testing.T) { + c := &MockTerminal{} + ss := NewTerminal(c, "> ") + line, err := ss.ReadLine() + if line != "" { + t.Errorf("Expected empty line but got: %s", line) + } + if err != io.EOF { + t.Errorf("Error should have been EOF but got: %s", err) + } +} + +var keyPressTests = []struct { + in string + line string + err error +}{ + { + "", + "", + io.EOF, + }, + { + "\r", + "", + nil, + }, + { + "foo\r", + "foo", + nil, + }, + { + "a\x1b[Cb\r", // right + "ab", + nil, + }, + { + "a\x1b[Db\r", // left + "ba", + nil, + }, + { + "a\177b\r", // backspace + "b", + nil, + }, +} + +func TestKeyPresses(t *testing.T) { + for i, test := range keyPressTests { + for j := 0; j < len(test.in); j++ { + c := &MockTerminal{ + toSend: []byte(test.in), + bytesPerRead: j, + } + ss := NewTerminal(c, "> ") + line, err := ss.ReadLine() + if line != test.line { + t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line) + break + } + if err != test.err { + t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) + break + } + } + } +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..67b287c --- /dev/null +++ b/util.go @@ -0,0 +1,115 @@ +// 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. + +// +build linux + +// Package terminal provides support functions for dealing with terminals, as +// commonly found on UNIX systems. +// +// Putting a terminal into raw mode is the most common requirement: +// +// oldState, err := terminal.MakeRaw(0) +// if err != nil { +// panic(err) +// } +// defer terminal.Restore(0, oldState) +package terminal + +import ( + "io" + "syscall" + "unsafe" +) + +// State contains the state of a terminal. +type State struct { + termios syscall.Termios +} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + var termios syscall.Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + return err == 0 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + return nil, err + } + + newState := oldState.termios + newState.Iflag &^= syscall.ISTRIP | syscall.INLCR | syscall.ICRNL | syscall.IGNCR | syscall.IXON | syscall.IXOFF + newState.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + return nil, err + } + + return &oldState, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) + return err +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + var dimensions [4]uint16 + + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 { + return -1, -1, err + } + return int(dimensions[1]), int(dimensions[0]), nil +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + var oldState syscall.Termios + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { + return nil, err + } + + newState := oldState + newState.Lflag &^= syscall.ECHO + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + return nil, err + } + + defer func() { + syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) + }() + + var buf [16]byte + var ret []byte + for { + n, err := syscall.Read(fd, buf[:]) + if err != nil { + return nil, err + } + if n == 0 { + if len(ret) == 0 { + return nil, io.EOF + } + break + } + if buf[n-1] == '\n' { + n-- + } + ret = append(ret, buf[:n]...) + if n < len(buf) { + break + } + } + + return ret, nil +} From 24437a6651182ec2c95561622ef74f57b6884be3 Mon Sep 17 00:00:00 2001 From: Dave Cheney Date: Tue, 9 Oct 2012 13:15:42 +1100 Subject: [PATCH 02/58] go.crypto: various: fix appengine compatibility Fixes golang/go#4102. R=russross, minux.ma, rsc, agl CC=golang-dev https://golang.org/cl/6623053 --- util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util.go b/util.go index 67b287c..daa36d7 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux +// +build linux,!appengine // Package terminal provides support functions for dealing with terminals, as // commonly found on UNIX systems. From ae4bfd41ff922e06afa9d92b53b9611d2c55cf27 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Sat, 22 Dec 2012 11:02:28 -0500 Subject: [PATCH 03/58] ssh/terminal: add GetState and make ReadPassword work in raw mode. GetState is useful for restoring the terminal in a signal handler. R=golang-dev, rsc CC=golang-dev https://golang.org/cl/6990043 --- util.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/util.go b/util.go index daa36d7..93dbbda 100644 --- a/util.go +++ b/util.go @@ -53,6 +53,17 @@ func MakeRaw(fd int) (*State, error) { return &oldState, nil } +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + return nil, err + } + + return &oldState, nil +} + // Restore restores the terminal connected to the given file descriptor to a // previous state. func Restore(fd int, state *State) error { @@ -81,6 +92,8 @@ func ReadPassword(fd int) ([]byte, error) { newState := oldState newState.Lflag &^= syscall.ECHO + newState.Lflag |= syscall.ICANON | syscall.ISIG + newState.Iflag |= syscall.ICRNL if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { return nil, err } From 78f827c12602320dc1d67e82d8594aa1a5800250 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Mon, 4 Feb 2013 10:36:09 -0500 Subject: [PATCH 04/58] ssh/terminal: add darwin support. terminal contains a number of utility functions that are currently only implemented for Linux. Darwin uses different named constants for getting and setting the terminal state so this change splits them off as constants and defines them for each arch. R=golang-dev, minux.ma CC=golang-dev https://golang.org/cl/7286043 --- util.go | 18 +++++++++--------- util_bsd.go | 12 ++++++++++++ util_linux.go | 12 ++++++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 util_bsd.go create mode 100644 util_linux.go diff --git a/util.go b/util.go index 93dbbda..8df94f5 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux,!appengine +// +build linux,!appengine darwin // Package terminal provides support functions for dealing with terminals, as // commonly found on UNIX systems. @@ -30,7 +30,7 @@ type State struct { // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd int) bool { var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) return err == 0 } @@ -39,14 +39,14 @@ func IsTerminal(fd int) bool { // restored. func MakeRaw(fd int) (*State, error) { var oldState State - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { return nil, err } newState := oldState.termios newState.Iflag &^= syscall.ISTRIP | syscall.INLCR | syscall.ICRNL | syscall.IGNCR | syscall.IXON | syscall.IXOFF newState.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { return nil, err } @@ -57,7 +57,7 @@ func MakeRaw(fd int) (*State, error) { // restore the terminal after a signal. func GetState(fd int) (*State, error) { var oldState State - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { return nil, err } @@ -67,7 +67,7 @@ func GetState(fd int) (*State, error) { // Restore restores the terminal connected to the given file descriptor to a // previous state. func Restore(fd int, state *State) error { - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) return err } @@ -86,7 +86,7 @@ func GetSize(fd int) (width, height int, err error) { // returned does not include the \n. func ReadPassword(fd int) ([]byte, error) { var oldState syscall.Termios - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { return nil, err } @@ -94,12 +94,12 @@ func ReadPassword(fd int) ([]byte, error) { newState.Lflag &^= syscall.ECHO newState.Lflag |= syscall.ICANON | syscall.ISIG newState.Iflag |= syscall.ICRNL - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { return nil, err } defer func() { - syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) + syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) }() var buf [16]byte diff --git a/util_bsd.go b/util_bsd.go new file mode 100644 index 0000000..1654453 --- /dev/null +++ b/util_bsd.go @@ -0,0 +1,12 @@ +// Copyright 2013 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. + +// +build darwin + +package terminal + +import "syscall" + +const ioctlReadTermios = syscall.TIOCGETA +const ioctlWriteTermios = syscall.TIOCSETA diff --git a/util_linux.go b/util_linux.go new file mode 100644 index 0000000..283144b --- /dev/null +++ b/util_linux.go @@ -0,0 +1,12 @@ +// Copyright 2013 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. + +// +build linux + +package terminal + +import "syscall" + +const ioctlReadTermios = syscall.TCGETS +const ioctlWriteTermios = syscall.TCSETS From 7d4f6f0986732548c5c620acb3fc56ba3d06ca13 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Fri, 7 Jun 2013 10:21:53 -0400 Subject: [PATCH 05/58] ssh/terminal: support home, end, up and down keys. R=golang-dev, dave CC=golang-dev https://golang.org/cl/9777043 --- terminal.go | 138 +++++++++++++++++++++++++++++++++++++++++------ terminal_test.go | 63 ++++++++++++++-------- 2 files changed, 163 insertions(+), 38 deletions(-) diff --git a/terminal.go b/terminal.go index c1ed0c0..d956b51 100644 --- a/terminal.go +++ b/terminal.go @@ -75,6 +75,17 @@ type Terminal struct { // a read. It aliases into inBuf. remainder []byte inBuf [256]byte + + // history contains previously entered commands so that they can be + // accessed with the up and down keys. + history stRingBuffer + // historyIndex stores the currently accessed history entry, where zero + // means the immediately previous entry. + historyIndex int + // When navigating up and down the history it's possible to return to + // the incomplete, initial line. That value is stored in + // historyPending. + historyPending string } // NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is @@ -83,12 +94,13 @@ type Terminal struct { // "> "). func NewTerminal(c io.ReadWriter, prompt string) *Terminal { return &Terminal{ - Escape: &vt100EscapeCodes, - c: c, - prompt: prompt, - termWidth: 80, - termHeight: 24, - echo: true, + Escape: &vt100EscapeCodes, + c: c, + prompt: prompt, + termWidth: 80, + termHeight: 24, + echo: true, + historyIndex: -1, } } @@ -104,6 +116,8 @@ const ( keyRight keyAltLeft keyAltRight + keyHome + keyEnd ) // bytesToKey tries to parse a key sequence from b. If successful, it returns @@ -130,6 +144,15 @@ func bytesToKey(b []byte) (int, []byte) { } } + if len(b) >= 3 && b[0] == keyEscape && b[1] == 'O' { + switch b[2] { + case 'H': + return keyHome, b[3:] + case 'F': + return keyEnd, b[3:] + } + } + if len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { switch b[5] { case 'C': @@ -238,6 +261,19 @@ func (t *Terminal) clearLineToRight() { const maxLineLength = 4096 +func (t *Terminal) setLine(newLine []byte, newPos int) { + if t.echo { + t.moveCursorToPos(0) + t.writeLine(newLine) + for i := len(newLine); i < len(t.line); i++ { + t.writeLine(space) + } + t.moveCursorToPos(newPos) + } + t.line = newLine + t.pos = newPos +} + // handleKey processes the given key and, optionally, returns a line of text // that the user has entered. func (t *Terminal) handleKey(key int) (line string, ok bool) { @@ -303,6 +339,42 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) { } t.pos++ t.moveCursorToPos(t.pos) + case keyHome: + if t.pos == 0 { + return + } + t.pos = 0 + t.moveCursorToPos(t.pos) + case keyEnd: + if t.pos == len(t.line) { + return + } + t.pos = len(t.line) + t.moveCursorToPos(t.pos) + case keyUp: + entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) + if !ok { + return "", false + } + if t.historyIndex == -1 { + t.historyPending = string(t.line) + } + t.historyIndex++ + t.setLine([]byte(entry), len(entry)) + case keyDown: + switch t.historyIndex { + case -1: + return + case 0: + t.setLine([]byte(t.historyPending), len(t.historyPending)) + t.historyIndex-- + default: + entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) + if ok { + t.historyIndex-- + t.setLine([]byte(entry), len(entry)) + } + } case keyEnter: t.moveCursorToPos(len(t.line)) t.queue([]byte("\r\n")) @@ -320,16 +392,7 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) { t.lock.Lock() if newLine != nil { - if t.echo { - t.moveCursorToPos(0) - t.writeLine(newLine) - for i := len(newLine); i < len(t.line); i++ { - t.writeLine(space) - } - t.moveCursorToPos(newPos) - } - t.line = newLine - t.pos = newPos + t.setLine(newLine, newPos) return } } @@ -483,6 +546,8 @@ func (t *Terminal) readLine() (line string, err error) { t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] if lineOk { + t.historyIndex = -1 + t.history.Add(line) return } @@ -501,7 +566,6 @@ func (t *Terminal) readLine() (line string, err error) { t.remainder = t.inBuf[:n+len(t.remainder)] } - panic("unreachable") } // SetPrompt sets the prompt to be used when reading subsequent lines. @@ -518,3 +582,43 @@ func (t *Terminal) SetSize(width, height int) { t.termWidth, t.termHeight = width, height } + +// stRingBuffer is a ring buffer of strings. +type stRingBuffer struct { + // entries contains max elements. + entries []string + max int + // head contains the index of the element most recently added to the ring. + head int + // size contains the number of elements in the ring. + size int +} + +func (s *stRingBuffer) Add(a string) { + if s.entries == nil { + const defaultNumEntries = 100 + s.entries = make([]string, defaultNumEntries) + s.max = defaultNumEntries + } + + s.head = (s.head + 1) % s.max + s.entries[s.head] = a + if s.size < s.max { + s.size++ + } +} + +// NthPreviousEntry returns the value passed to the nth previous call to Add. +// If n is zero then the immediately prior value is returned, if one, then the +// next most recent, and so on. If such an element doesn't exist then ok is +// false. +func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { + if n >= s.size { + return "", false + } + index := s.head - n + if index < 0 { + index += s.max + } + return s.entries[index], true +} diff --git a/terminal_test.go b/terminal_test.go index a219721..ffcda79 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -52,39 +52,54 @@ func TestClose(t *testing.T) { } var keyPressTests = []struct { - in string - line string - err error + in string + line string + err error + throwAwayLines int }{ { - "", - "", - io.EOF, + err: io.EOF, }, { - "\r", - "", - nil, + in: "\r", + line: "", }, { - "foo\r", - "foo", - nil, + in: "foo\r", + line: "foo", }, { - "a\x1b[Cb\r", // right - "ab", - nil, + in: "a\x1b[Cb\r", // right + line: "ab", }, { - "a\x1b[Db\r", // left - "ba", - nil, + in: "a\x1b[Db\r", // left + line: "ba", }, { - "a\177b\r", // backspace - "b", - nil, + in: "a\177b\r", // backspace + line: "b", + }, + { + in: "\x1b[A\r", // up + }, + { + in: "\x1b[B\r", // down + }, + { + in: "line\x1b[A\x1b[B\r", // up then down + line: "line", + }, + { + in: "line1\rline2\x1b[A\r", // recall previous line. + line: "line1", + throwAwayLines: 1, + }, + { + // recall two previous lines and append. + in: "line1\rline2\rline3\x1b[A\x1b[Axxx\r", + line: "line1xxx", + throwAwayLines: 2, }, } @@ -96,6 +111,12 @@ func TestKeyPresses(t *testing.T) { bytesPerRead: j, } ss := NewTerminal(c, "> ") + for k := 0; k < test.throwAwayLines; k++ { + _, err := ss.ReadLine() + if err != nil { + t.Errorf("Throwaway line %d from test %d resulted in error: %s", k, i, err) + } + } line, err := ss.ReadLine() if line != test.line { t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line) From 329344838903394549d2d0823f13202fdb08d4fb Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Tue, 2 Jul 2013 19:46:13 -0400 Subject: [PATCH 06/58] go.crypto/ssh/terminal: don't save passwords in history. The history buffer would recall previously entered lines: including passwords. With this change, lines entered while echo is disabled are no longer put into the history. R=golang-dev, rsc CC=golang-dev https://golang.org/cl/10853043 --- terminal.go | 6 ++++-- terminal_test.go | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/terminal.go b/terminal.go index d956b51..f83be8c 100644 --- a/terminal.go +++ b/terminal.go @@ -546,8 +546,10 @@ func (t *Terminal) readLine() (line string, err error) { t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] if lineOk { - t.historyIndex = -1 - t.history.Add(line) + if t.echo { + t.historyIndex = -1 + t.history.Add(line) + } return } diff --git a/terminal_test.go b/terminal_test.go index ffcda79..7db3171 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -129,3 +129,19 @@ func TestKeyPresses(t *testing.T) { } } } + +func TestPasswordNotSaved(t *testing.T) { + c := &MockTerminal{ + toSend: []byte("password\r\x1b[A\r"), + bytesPerRead: 1, + } + ss := NewTerminal(c, "> ") + pw, _ := ss.ReadPassword("> ") + if pw != "password" { + t.Fatalf("failed to read password, got %s", pw) + } + line, _ := ss.ReadLine() + if len(line) > 0 { + t.Fatalf("password was saved in history") + } +} From 89c74edf6604a99d603155e3ebd4f6e80eb2fa20 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Mon, 15 Jul 2013 18:01:31 -0400 Subject: [PATCH 07/58] go.crypto/ssh/terminal: support Go 1.0. For those still stuck on Go 1.0. R=golang-dev, rsc CC=golang-dev https://golang.org/cl/11297043 --- terminal.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terminal.go b/terminal.go index f83be8c..411b1f1 100644 --- a/terminal.go +++ b/terminal.go @@ -568,6 +568,8 @@ func (t *Terminal) readLine() (line string, err error) { t.remainder = t.inBuf[:n+len(t.remainder)] } + + panic("unreachable") // for Go 1.0. } // SetPrompt sets the prompt to be used when reading subsequent lines. From 78276a84eec125ea64bd1dcdc349050f251da1b2 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Wed, 28 Aug 2013 07:36:04 -0400 Subject: [PATCH 08/58] go.crypto/ssh/terminal: handle ^W, ^K and ^H R=golang-dev, bradfitz CC=golang-dev https://golang.org/cl/13207043 --- terminal.go | 132 +++++++++++++++++++++++++++++++++-------------- terminal_test.go | 38 +++++++++++++- 2 files changed, 130 insertions(+), 40 deletions(-) diff --git a/terminal.go b/terminal.go index 411b1f1..bb69c5b 100644 --- a/terminal.go +++ b/terminal.go @@ -118,6 +118,8 @@ const ( keyAltRight keyHome keyEnd + keyDeleteWord + keyDeleteLine ) // bytesToKey tries to parse a key sequence from b. If successful, it returns @@ -127,6 +129,15 @@ func bytesToKey(b []byte) (int, []byte) { return -1, nil } + switch b[0] { + case 8: // ^H + return keyBackspace, b[1:] + case 11: // ^K + return keyDeleteLine, b[1:] + case 23: // ^W + return keyDeleteWord, b[1:] + } + if b[0] != keyEscape { return int(b[0]), b[1:] } @@ -274,6 +285,73 @@ func (t *Terminal) setLine(newLine []byte, newPos int) { t.pos = newPos } +func (t *Terminal) eraseNPreviousChars(n int) { + if n == 0 { + return + } + + if t.pos < n { + n = t.pos + } + t.pos -= n + t.moveCursorToPos(t.pos) + + copy(t.line[t.pos:], t.line[n+t.pos:]) + t.line = t.line[:len(t.line)-n] + if t.echo { + t.writeLine(t.line[t.pos:]) + for i := 0; i < n; i++ { + t.queue(space) + } + t.cursorX += n + t.moveCursorToPos(t.pos) + } +} + +// countToLeftWord returns then number of characters from the cursor to the +// start of the previous word. +func (t *Terminal) countToLeftWord() int { + if t.pos == 0 { + return 0 + } + + pos := t.pos - 1 + for pos > 0 { + if t.line[pos] != ' ' { + break + } + pos-- + } + for pos > 0 { + if t.line[pos] == ' ' { + pos++ + break + } + pos-- + } + + return t.pos - pos +} + +// countToRightWord returns then number of characters from the cursor to the +// start of the next word. +func (t *Terminal) countToRightWord() int { + pos := t.pos + for pos < len(t.line) { + if t.line[pos] == ' ' { + break + } + pos++ + } + for pos < len(t.line) { + if t.line[pos] != ' ' { + break + } + pos++ + } + return pos - t.pos +} + // handleKey processes the given key and, optionally, returns a line of text // that the user has entered. func (t *Terminal) handleKey(key int) (line string, ok bool) { @@ -282,50 +360,14 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) { if t.pos == 0 { return } - t.pos-- - t.moveCursorToPos(t.pos) - - copy(t.line[t.pos:], t.line[1+t.pos:]) - t.line = t.line[:len(t.line)-1] - if t.echo { - t.writeLine(t.line[t.pos:]) - } - t.queue(eraseUnderCursor) - t.moveCursorToPos(t.pos) + t.eraseNPreviousChars(1) case keyAltLeft: // move left by a word. - if t.pos == 0 { - return - } - t.pos-- - for t.pos > 0 { - if t.line[t.pos] != ' ' { - break - } - t.pos-- - } - for t.pos > 0 { - if t.line[t.pos] == ' ' { - t.pos++ - break - } - t.pos-- - } + t.pos -= t.countToLeftWord() t.moveCursorToPos(t.pos) case keyAltRight: // move right by a word. - for t.pos < len(t.line) { - if t.line[t.pos] == ' ' { - break - } - t.pos++ - } - for t.pos < len(t.line) { - if t.line[t.pos] != ' ' { - break - } - t.pos++ - } + t.pos += t.countToRightWord() t.moveCursorToPos(t.pos) case keyLeft: if t.pos == 0 { @@ -385,6 +427,18 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) { t.cursorX = 0 t.cursorY = 0 t.maxLine = 0 + case keyDeleteWord: + // Delete zero or more spaces and then one or more characters. + t.eraseNPreviousChars(t.countToLeftWord()) + case keyDeleteLine: + // Delete everything from the current cursor position to the + // end of line. + for i := t.pos; i < len(t.line); i++ { + t.queue(space) + t.cursorX++ + } + t.line = t.line[:t.pos] + t.moveCursorToPos(t.pos) default: if t.AutoCompleteCallback != nil { t.lock.Unlock() diff --git a/terminal_test.go b/terminal_test.go index 7db3171..75584d3 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -101,11 +101,47 @@ var keyPressTests = []struct { line: "line1xxx", throwAwayLines: 2, }, + { + in: "\027\r", + line: "", + }, + { + in: "a\027\r", + line: "", + }, + { + in: "a \027\r", + line: "", + }, + { + in: "a b\027\r", + line: "a ", + }, + { + in: "a b \027\r", + line: "a ", + }, + { + in: "one two thr\x1b[D\027\r", + line: "one two r", + }, + { + in: "\013\r", + line: "", + }, + { + in: "a\013\r", + line: "a", + }, + { + in: "ab\x1b[D\013\r", + line: "a", + }, } func TestKeyPresses(t *testing.T) { for i, test := range keyPressTests { - for j := 0; j < len(test.in); j++ { + for j := 1; j < len(test.in); j++ { c := &MockTerminal{ toSend: []byte(test.in), bytesPerRead: j, From 1b08228a8ad25476dc83b471d9e64e2790ebb1a6 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Fri, 13 Sep 2013 14:33:00 -0400 Subject: [PATCH 09/58] go.crypto/ssh/terminal: support Unicode entry. Previously, terminal only supported ASCII characters. This change alters some []byte to []rune so that the full range of Unicode is supported. The only thing that doesn't appear to work correctly are grapheme clusters as the code still assumes one rune per glyph. Still, this change allows many more languages to work than did previously. R=golang-dev, rsc CC=golang-dev https://golang.org/cl/13704043 --- terminal.go | 82 +++++++++++++++++++++++++++--------------------- terminal_test.go | 4 +++ 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/terminal.go b/terminal.go index bb69c5b..a0ddba3 100644 --- a/terminal.go +++ b/terminal.go @@ -7,6 +7,7 @@ package terminal import ( "io" "sync" + "unicode/utf8" ) // EscapeCodes contains escape sequences that can be written to the terminal in @@ -35,11 +36,12 @@ var vt100EscapeCodes = EscapeCodes{ // Terminal contains the state for running a VT100 terminal that is capable of // reading lines of input. type Terminal struct { - // AutoCompleteCallback, if non-null, is called for each keypress - // with the full input line and the current position of the cursor. - // If it returns a nil newLine, the key press is processed normally. - // Otherwise it returns a replacement line and the new cursor position. - AutoCompleteCallback func(line []byte, pos, key int) (newLine []byte, newPos int) + // AutoCompleteCallback, if non-null, is called for each keypress with + // the full input line and the current position of the cursor (in + // bytes, as an index into |line|). If it returns ok=false, the key + // press is processed normally. Otherwise it returns a replacement line + // and the new cursor position. + AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) // Escape contains a pointer to the escape codes for this terminal. // It's always a valid pointer, although the escape codes themselves @@ -54,7 +56,7 @@ type Terminal struct { prompt string // line is the current line being entered. - line []byte + line []rune // pos is the logical position of the cursor in line pos int // echo is true if local echo is enabled @@ -109,7 +111,7 @@ const ( keyEnter = '\r' keyEscape = 27 keyBackspace = 127 - keyUnknown = 256 + iota + keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota keyUp keyDown keyLeft @@ -123,10 +125,10 @@ const ( ) // 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) { +// the key and the remainder of the input. Otherwise it returns utf8.RuneError. +func bytesToKey(b []byte) (rune, []byte) { if len(b) == 0 { - return -1, nil + return utf8.RuneError, nil } switch b[0] { @@ -139,7 +141,11 @@ func bytesToKey(b []byte) (int, []byte) { } if b[0] != keyEscape { - return int(b[0]), b[1:] + if !utf8.FullRune(b) { + return utf8.RuneError, b + } + r, l := utf8.DecodeRune(b) + return r, b[l:] } if len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { @@ -183,19 +189,20 @@ func bytesToKey(b []byte) (int, []byte) { } } - return -1, b + return utf8.RuneError, b } // queue appends data to the end of t.outBuf -func (t *Terminal) queue(data []byte) { - t.outBuf = append(t.outBuf, data...) +func (t *Terminal) queue(data []rune) { + t.outBuf = append(t.outBuf, []byte(string(data))...) } -var eraseUnderCursor = []byte{' ', keyEscape, '[', 'D'} -var space = []byte{' '} +var eraseUnderCursor = []rune{' ', keyEscape, '[', 'D'} +var space = []rune{' '} -func isPrintable(key int) bool { - return key >= 32 && key < 127 +func isPrintable(key rune) bool { + isInSurrogateArea := key >= 0xd800 && key <= 0xdbff + return key >= 32 && !isInSurrogateArea } // moveCursorToPos appends data to t.outBuf which will move the cursor to the @@ -235,7 +242,7 @@ func (t *Terminal) moveCursorToPos(pos int) { } func (t *Terminal) move(up, down, left, right int) { - movement := make([]byte, 3*(up+down+left+right)) + movement := make([]rune, 3*(up+down+left+right)) m := movement for i := 0; i < up; i++ { m[0] = keyEscape @@ -266,13 +273,13 @@ func (t *Terminal) move(up, down, left, right int) { } func (t *Terminal) clearLineToRight() { - op := []byte{keyEscape, '[', 'K'} + op := []rune{keyEscape, '[', 'K'} t.queue(op) } const maxLineLength = 4096 -func (t *Terminal) setLine(newLine []byte, newPos int) { +func (t *Terminal) setLine(newLine []rune, newPos int) { if t.echo { t.moveCursorToPos(0) t.writeLine(newLine) @@ -354,7 +361,7 @@ func (t *Terminal) countToRightWord() int { // handleKey processes the given key and, optionally, returns a line of text // that the user has entered. -func (t *Terminal) handleKey(key int) (line string, ok bool) { +func (t *Terminal) handleKey(key rune) (line string, ok bool) { switch key { case keyBackspace: if t.pos == 0 { @@ -402,24 +409,24 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) { t.historyPending = string(t.line) } t.historyIndex++ - t.setLine([]byte(entry), len(entry)) + t.setLine([]rune(entry), len(entry)) case keyDown: switch t.historyIndex { case -1: return case 0: - t.setLine([]byte(t.historyPending), len(t.historyPending)) + t.setLine([]rune(t.historyPending), len(t.historyPending)) t.historyIndex-- default: entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) if ok { t.historyIndex-- - t.setLine([]byte(entry), len(entry)) + t.setLine([]rune(entry), len(entry)) } } case keyEnter: t.moveCursorToPos(len(t.line)) - t.queue([]byte("\r\n")) + t.queue([]rune("\r\n")) line = string(t.line) ok = true t.line = t.line[:0] @@ -441,12 +448,15 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) { t.moveCursorToPos(t.pos) default: if t.AutoCompleteCallback != nil { + prefix := string(t.line[:t.pos]) + suffix := string(t.line[t.pos:]) + t.lock.Unlock() - newLine, newPos := t.AutoCompleteCallback(t.line, t.pos, key) + newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) t.lock.Lock() - if newLine != nil { - t.setLine(newLine, newPos) + if completeOk { + t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) return } } @@ -457,13 +467,13 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) { return } if len(t.line) == cap(t.line) { - newLine := make([]byte, len(t.line), 2*(1+len(t.line))) + newLine := make([]rune, len(t.line), 2*(1+len(t.line))) copy(newLine, t.line) t.line = newLine } t.line = t.line[:len(t.line)+1] copy(t.line[t.pos+1:], t.line[t.pos:]) - t.line[t.pos] = byte(key) + t.line[t.pos] = key if t.echo { t.writeLine(t.line[t.pos:]) } @@ -473,7 +483,7 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) { return } -func (t *Terminal) writeLine(line []byte) { +func (t *Terminal) writeLine(line []rune) { for len(line) != 0 { remainingOnLine := t.termWidth - t.cursorX todo := len(line) @@ -525,7 +535,7 @@ func (t *Terminal) Write(buf []byte) (n int, err error) { return } - t.queue([]byte(t.prompt)) + t.queue([]rune(t.prompt)) chars := len(t.prompt) if t.echo { t.queue(t.line) @@ -572,7 +582,7 @@ func (t *Terminal) readLine() (line string, err error) { // t.lock must be held at this point if t.cursorX == 0 && t.cursorY == 0 { - t.writeLine([]byte(t.prompt)) + t.writeLine([]rune(t.prompt)) t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] } @@ -581,9 +591,9 @@ func (t *Terminal) readLine() (line string, err error) { rest := t.remainder lineOk := false for !lineOk { - var key int + var key rune key, rest = bytesToKey(rest) - if key < 0 { + if key == utf8.RuneError { break } if key == keyCtrlD { diff --git a/terminal_test.go b/terminal_test.go index 75584d3..6ea92d9 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -137,6 +137,10 @@ var keyPressTests = []struct { in: "ab\x1b[D\013\r", line: "a", }, + { + in: "Ξεσκεπάζω\r", + line: "Ξεσκεπάζω", + }, } func TestKeyPresses(t *testing.T) { From 4986295c478039f90720a1bc8a1b6b3991f6d89d Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Sun, 15 Sep 2013 12:48:02 -0400 Subject: [PATCH 10/58] go.crypto/ssh/terminal: fix non-ASCII history. The length of history buffer entries (which are stored as strings) was being used as the number of runes. This was correct until ff9ce887b46b, which allowed unicode entry, but can now cause a panic when editing history that contains non-ASCII codepoints. R=golang-dev, sfrithjof, r CC=golang-dev https://golang.org/cl/13255050 --- terminal.go | 9 ++++++--- terminal_test.go | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/terminal.go b/terminal.go index a0ddba3..66439cf 100644 --- a/terminal.go +++ b/terminal.go @@ -409,19 +409,22 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { t.historyPending = string(t.line) } t.historyIndex++ - t.setLine([]rune(entry), len(entry)) + runes := []rune(entry) + t.setLine(runes, len(runes)) case keyDown: switch t.historyIndex { case -1: return case 0: - t.setLine([]rune(t.historyPending), len(t.historyPending)) + runes := []rune(t.historyPending) + t.setLine(runes, len(runes)) t.historyIndex-- default: entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) if ok { t.historyIndex-- - t.setLine([]rune(entry), len(entry)) + runes := []rune(entry) + t.setLine(runes, len(runes)) } } case keyEnter: diff --git a/terminal_test.go b/terminal_test.go index 6ea92d9..7fbf0e6 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -141,6 +141,16 @@ var keyPressTests = []struct { in: "Ξεσκεπάζω\r", line: "Ξεσκεπάζω", }, + { + in: "£\r\x1b[A\177\r", // non-ASCII char, enter, up, backspace. + line: "", + throwAwayLines: 1, + }, + { + in: "£\r££\x1b[A\x1b[B\177\r", // non-ASCII char, enter, 2x non-ASCII, up, down, backspace, enter. + line: "£", + throwAwayLines: 1, + }, } func TestKeyPresses(t *testing.T) { From 879e95502452ccc02c92279e6f13fa1a9798aca7 Mon Sep 17 00:00:00 2001 From: Frithjof Schulze Date: Mon, 30 Sep 2013 16:06:07 -0400 Subject: [PATCH 11/58] go.crypto/ssh/terminal: Allow ^A and ^E as synonyms for Home and End. I understand that ssh/terminal can't implement everybodys favorite keyboard shortcuts, but I think these are very widespread. They exist not only in Emacs or Readline, but also in Acme and Sam. Also they almost come for free. R=golang-dev CC=agl, golang-dev https://golang.org/cl/13839047 --- terminal.go | 4 ++++ terminal_test.go | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/terminal.go b/terminal.go index 66439cf..86853d6 100644 --- a/terminal.go +++ b/terminal.go @@ -132,6 +132,10 @@ func bytesToKey(b []byte) (rune, []byte) { } switch b[0] { + case 1: // ^A + return keyHome, b[1:] + case 5: // ^E + return keyEnd, b[1:] case 8: // ^H return keyBackspace, b[1:] case 11: // ^K diff --git a/terminal_test.go b/terminal_test.go index 7fbf0e6..641576c 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -101,6 +101,18 @@ var keyPressTests = []struct { line: "line1xxx", throwAwayLines: 2, }, + { + // Ctrl-A to move to beginning of line followed by ^K to kill + // line. + in: "a b \001\013\r", + line: "", + }, + { + // Ctrl-A to move to beginning of line, Ctrl-E to move to end, + // finally ^K to kill nothing. + in: "a b \001\005\013\r", + line: "a b ", + }, { in: "\027\r", line: "", From 1fcadde6c1bde20b13dee050cf7585a00f69b489 Mon Sep 17 00:00:00 2001 From: Michael Gehring Date: Mon, 13 Jan 2014 14:35:19 -0800 Subject: [PATCH 12/58] go.crypto/ssh/terminal: enable freebsd build syscall.Termios, which was the only thing breaking the build, is available in go tip now (https://code.google.com/p/go/source/detail?r=873d664b00ec) R=golang-codereviews, bradfitz CC=golang-codereviews https://golang.org/cl/51690043 --- util.go | 2 +- util_bsd.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/util.go b/util.go index 8df94f5..84c7d1e 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux,!appengine darwin +// +build linux,!appengine darwin freebsd // Package terminal provides support functions for dealing with terminals, as // commonly found on UNIX systems. diff --git a/util_bsd.go b/util_bsd.go index 1654453..91b5834 100644 --- a/util_bsd.go +++ b/util_bsd.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build darwin +// +build darwin freebsd package terminal From 218fdea9b778e95327d4c260f8954c79d6a90f97 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 13 Jan 2014 15:00:50 -0800 Subject: [PATCH 13/58] undo CL 51690043 / abf8f8812575 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaks FreeBSD build of subrepo for non-tip users. ««« original CL description go.crypto/ssh/terminal: enable freebsd build syscall.Termios, which was the only thing breaking the build, is available in go tip now (https://code.google.com/p/go/source/detail?r=873d664b00ec) R=golang-codereviews, bradfitz CC=golang-codereviews https://golang.org/cl/51690043 »»» R=golang-codereviews, dave CC=golang-codereviews https://golang.org/cl/51100044 --- util.go | 2 +- util_bsd.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/util.go b/util.go index 84c7d1e..8df94f5 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux,!appengine darwin freebsd +// +build linux,!appengine darwin // Package terminal provides support functions for dealing with terminals, as // commonly found on UNIX systems. diff --git a/util_bsd.go b/util_bsd.go index 91b5834..1654453 100644 --- a/util_bsd.go +++ b/util_bsd.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build darwin freebsd +// +build darwin package terminal From d5f42d23b56e59e1cf6927cdcaed8011db5fbf9d Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Wed, 9 Apr 2014 13:57:52 -0700 Subject: [PATCH 14/58] go.crypto/ssh: import gosshnew. See https://groups.google.com/d/msg/Golang-nuts/AoVxQ4bB5XQ/i8kpMxdbVlEJ R=hanwen CC=golang-codereviews https://golang.org/cl/86190043 --- util_windows.go | 171 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 util_windows.go diff --git a/util_windows.go b/util_windows.go new file mode 100644 index 0000000..0a454e0 --- /dev/null +++ b/util_windows.go @@ -0,0 +1,171 @@ +// 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. + +// +build windows + +// Package terminal provides support functions for dealing with terminals, as +// commonly found on UNIX systems. +// +// Putting a terminal into raw mode is the most common requirement: +// +// oldState, err := terminal.MakeRaw(0) +// if err != nil { +// panic(err) +// } +// defer terminal.Restore(0, oldState) +package terminal + +import ( + "io" + "syscall" + "unsafe" +) + +const ( + enableLineInput = 2 + enableEchoInput = 4 + enableProcessedInput = 1 + enableWindowInput = 8 + enableMouseInput = 16 + enableInsertMode = 32 + enableQuickEditMode = 64 + enableExtendedFlags = 128 + enableAutoPosition = 256 + enableProcessedOutput = 1 + enableWrapAtEolOutput = 2 +) + +var kernel32 = syscall.NewLazyDLL("kernel32.dll") + +var ( + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procSetConsoleMode = kernel32.NewProc("SetConsoleMode") + procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") +) + +type ( + short int16 + word uint16 + + coord struct { + x short + y short + } + smallRect struct { + left short + top short + right short + bottom short + } + consoleScreenBufferInfo struct { + size coord + cursorPosition coord + attributes word + window smallRect + maximumWindowSize coord + } +) + +type State struct { + mode uint32 +} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + var st uint32 + r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) + return r != 0 && e == 0 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + var st uint32 + _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) + if e != 0 { + return nil, error(e) + } + st &^= (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput) + _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0) + if e != 0 { + return nil, error(e) + } + return &State{st}, nil +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + var st uint32 + _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) + if e != 0 { + return nil, error(e) + } + return &State{st}, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + _, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0) + return err +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + var info consoleScreenBufferInfo + _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0) + if e != 0 { + return 0, 0, error(e) + } + return int(info.size.x), int(info.size.y), nil +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + var st uint32 + _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) + if e != 0 { + return nil, error(e) + } + old := st + + st &^= (enableEchoInput) + st |= (enableProcessedInput | enableLineInput | enableProcessedOutput) + _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0) + if e != 0 { + return nil, error(e) + } + + defer func() { + syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0) + }() + + var buf [16]byte + var ret []byte + for { + n, err := syscall.Read(syscall.Handle(fd), buf[:]) + if err != nil { + return nil, err + } + if n == 0 { + if len(ret) == 0 { + return nil, io.EOF + } + break + } + if buf[n-1] == '\n' { + n-- + } + ret = append(ret, buf[:n]...) + if n < len(buf) { + break + } + } + + return ret, nil +} From ea2b43b38e8d870a263c26f44e04349a9feae5d1 Mon Sep 17 00:00:00 2001 From: Mikio Hara Date: Mon, 5 May 2014 12:07:22 -0700 Subject: [PATCH 15/58] go.crypto/ssh/terminal: add support for BSD variants LGTM=agl R=golang-codereviews, agl CC=golang-codereviews https://golang.org/cl/97850043 --- util.go | 2 +- util_bsd.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/util.go b/util.go index 8df94f5..0763c9a 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux,!appengine darwin +// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd // Package terminal provides support functions for dealing with terminals, as // commonly found on UNIX systems. diff --git a/util_bsd.go b/util_bsd.go index 1654453..9c1ffd1 100644 --- a/util_bsd.go +++ b/util_bsd.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build darwin +// +build darwin dragonfly freebsd netbsd openbsd package terminal From d47901289b793009abd5e262705b478916a2da8c Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Tue, 27 May 2014 19:45:07 -0700 Subject: [PATCH 16/58] go.crypto/ssh/terminal: support ^U, ^D and ^L. LGTM=bradfitz R=bradfitz, marios.nikolaou CC=golang-codereviews https://golang.org/cl/92220043 --- terminal.go | 25 ++++++++++++++++++++++++- terminal_test.go | 16 ++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/terminal.go b/terminal.go index 86853d6..18ac2ba 100644 --- a/terminal.go +++ b/terminal.go @@ -108,6 +108,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal { const ( keyCtrlD = 4 + keyCtrlU = 21 keyEnter = '\r' keyEscape = 27 keyBackspace = 127 @@ -122,6 +123,7 @@ const ( keyEnd keyDeleteWord keyDeleteLine + keyClearScreen ) // bytesToKey tries to parse a key sequence from b. If successful, it returns @@ -140,6 +142,8 @@ func bytesToKey(b []byte) (rune, []byte) { return keyBackspace, b[1:] case 11: // ^K return keyDeleteLine, b[1:] + case 12: // ^L + return keyClearScreen, b[1:] case 23: // ^W return keyDeleteWord, b[1:] } @@ -453,6 +457,23 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { } t.line = t.line[:t.pos] t.moveCursorToPos(t.pos) + case keyCtrlD: + // Erase the character under the current position. + // The EOF case when the line is empty is handled in + // readLine(). + if t.pos < len(t.line) { + t.pos++ + t.eraseNPreviousChars(1) + } + case keyCtrlU: + t.eraseNPreviousChars(t.pos) + case keyClearScreen: + // Erases the screen and moves the cursor to the home position. + t.queue([]rune("\x1b[2J\x1b[H")) + t.queue([]rune(t.prompt)) + t.cursorX = len(t.prompt) + t.cursorY = 0 + t.setLine(t.line, t.pos) default: if t.AutoCompleteCallback != nil { prefix := string(t.line[:t.pos]) @@ -604,7 +625,9 @@ func (t *Terminal) readLine() (line string, err error) { break } if key == keyCtrlD { - return "", io.EOF + if len(t.line) == 0 { + return "", io.EOF + } } line, lineOk = t.handleKey(key) } diff --git a/terminal_test.go b/terminal_test.go index 641576c..fb42d76 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -163,6 +163,22 @@ var keyPressTests = []struct { line: "£", throwAwayLines: 1, }, + { + // Ctrl-D at the end of the line should be ignored. + in: "a\004\r", + line: "a", + }, + { + // a, b, left, Ctrl-D should erase the b. + in: "ab\x1b[D\004\r", + line: "a", + }, + { + // a, b, c, d, left, left, ^U should erase to the beginning of + // the line. + in: "abcd\x1b[D\x1b[D\025\r", + line: "cd", + }, } func TestKeyPresses(t *testing.T) { From 196543343b7dc54e8600fe7d2840a56a576fe1e9 Mon Sep 17 00:00:00 2001 From: Dave Cheney Date: Mon, 7 Jul 2014 10:24:36 +1000 Subject: [PATCH 17/58] go.crypt/ssh/terminal: declare TCGETS, TCSETS constants locally. Currently the ssh/terminal package cannot be compiled under gccgo. Even though gccgo may be running on linux, its syscall package is slightly different and does not contain these constants. This proposal resolves the issue by declaring the two constants locally, as we've done for the *BSDs. LGTM=hanwen, iant R=hanwen, iant, gobot CC=golang-codereviews https://golang.org/cl/101670043 --- util_linux.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/util_linux.go b/util_linux.go index 283144b..5883b22 100644 --- a/util_linux.go +++ b/util_linux.go @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux - package terminal -import "syscall" - -const ioctlReadTermios = syscall.TCGETS -const ioctlWriteTermios = syscall.TCSETS +// These constants are declared here, rather than importing +// them from the syscall package as some syscall packages, even +// on linux, for example gccgo, do not declare them. +const ioctlReadTermios = 0x5401 // syscall.TCGETS +const ioctlWriteTermios = 0x5402 // syscall.TCSETS From e0a9d256f77a18c7bae4a44233a6a9d914a3f866 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Fri, 1 Aug 2014 11:22:47 -0700 Subject: [PATCH 18/58] go.crypto/ssh/terminal: better handling of window resizing. There doesn't appear to be perfect behaviour for line editing code in the face of terminal resizing. But this change works pretty well on xterm and gnome-terminal and certainly a lot better than it used to. LGTM=bradfitz R=bradfitz CC=golang-codereviews https://golang.org/cl/105580043 --- terminal.go | 143 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 27 deletions(-) diff --git a/terminal.go b/terminal.go index 18ac2ba..123de5e 100644 --- a/terminal.go +++ b/terminal.go @@ -53,7 +53,7 @@ type Terminal struct { lock sync.Mutex c io.ReadWriter - prompt string + prompt []rune // line is the current line being entered. line []rune @@ -98,7 +98,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal { return &Terminal{ Escape: &vt100EscapeCodes, c: c, - prompt: prompt, + prompt: []rune(prompt), termWidth: 80, termHeight: 24, echo: true, @@ -220,7 +220,7 @@ func (t *Terminal) moveCursorToPos(pos int) { return } - x := len(t.prompt) + pos + x := visualLength(t.prompt) + pos y := x / t.termWidth x = x % t.termWidth @@ -300,6 +300,29 @@ func (t *Terminal) setLine(newLine []rune, newPos int) { t.pos = newPos } +func (t *Terminal) advanceCursor(places int) { + t.cursorX += places + t.cursorY += t.cursorX / t.termWidth + if t.cursorY > t.maxLine { + t.maxLine = t.cursorY + } + t.cursorX = t.cursorX % t.termWidth + + if places > 0 && t.cursorX == 0 { + // Normally terminals will advance the current position + // when writing a character. But that doesn't happen + // for the last character in a line. However, when + // writing a character (except a new line) that causes + // a line wrap, the position will be advanced two + // places. + // + // So, if we are stopping at the end of a line, we + // need to write a newline so that our cursor can be + // advanced to the next line. + t.outBuf = append(t.outBuf, '\n') + } +} + func (t *Terminal) eraseNPreviousChars(n int) { if n == 0 { return @@ -318,7 +341,7 @@ func (t *Terminal) eraseNPreviousChars(n int) { for i := 0; i < n; i++ { t.queue(space) } - t.cursorX += n + t.advanceCursor(n) t.moveCursorToPos(t.pos) } } @@ -367,6 +390,27 @@ func (t *Terminal) countToRightWord() int { return pos - t.pos } +// visualLength returns the number of visible glyphs in s. +func visualLength(runes []rune) int { + inEscapeSeq := false + length := 0 + + for _, r := range runes { + switch { + case inEscapeSeq: + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + inEscapeSeq = false + } + case r == '\x1b': + inEscapeSeq = true + default: + length++ + } + } + + return length +} + // handleKey processes the given key and, optionally, returns a line of text // that the user has entered. func (t *Terminal) handleKey(key rune) (line string, ok bool) { @@ -453,7 +497,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { // end of line. for i := t.pos; i < len(t.line); i++ { t.queue(space) - t.cursorX++ + t.advanceCursor(1) } t.line = t.line[:t.pos] t.moveCursorToPos(t.pos) @@ -470,9 +514,9 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { case keyClearScreen: // Erases the screen and moves the cursor to the home position. t.queue([]rune("\x1b[2J\x1b[H")) - t.queue([]rune(t.prompt)) - t.cursorX = len(t.prompt) - t.cursorY = 0 + t.queue(t.prompt) + t.cursorX, t.cursorY = 0, 0 + t.advanceCursor(visualLength(t.prompt)) t.setLine(t.line, t.pos) default: if t.AutoCompleteCallback != nil { @@ -519,16 +563,8 @@ func (t *Terminal) writeLine(line []rune) { todo = remainingOnLine } t.queue(line[:todo]) - t.cursorX += todo + t.advanceCursor(visualLength(line[:todo])) line = line[todo:] - - if t.cursorX == t.termWidth { - t.cursorX = 0 - t.cursorY++ - if t.cursorY > t.maxLine { - t.maxLine = t.cursorY - } - } } } @@ -563,14 +599,11 @@ func (t *Terminal) Write(buf []byte) (n int, err error) { return } - t.queue([]rune(t.prompt)) - chars := len(t.prompt) + t.writeLine(t.prompt) if t.echo { - t.queue(t.line) - chars += len(t.line) + t.writeLine(t.line) } - t.cursorX = chars % t.termWidth - t.cursorY = chars / t.termWidth + t.moveCursorToPos(t.pos) if _, err = t.c.Write(t.outBuf); err != nil { @@ -587,7 +620,7 @@ func (t *Terminal) ReadPassword(prompt string) (line string, err error) { defer t.lock.Unlock() oldPrompt := t.prompt - t.prompt = prompt + t.prompt = []rune(prompt) t.echo = false line, err = t.readLine() @@ -610,7 +643,7 @@ func (t *Terminal) readLine() (line string, err error) { // t.lock must be held at this point if t.cursorX == 0 && t.cursorY == 0 { - t.writeLine([]rune(t.prompt)) + t.writeLine(t.prompt) t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] } @@ -671,14 +704,70 @@ func (t *Terminal) SetPrompt(prompt string) { t.lock.Lock() defer t.lock.Unlock() - t.prompt = prompt + t.prompt = []rune(prompt) } -func (t *Terminal) SetSize(width, height int) { +func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) { + // Move cursor to column zero at the start of the line. + t.move(t.cursorY, 0, t.cursorX, 0) + t.cursorX, t.cursorY = 0, 0 + t.clearLineToRight() + for t.cursorY < numPrevLines { + // Move down a line + t.move(0, 1, 0, 0) + t.cursorY++ + t.clearLineToRight() + } + // Move back to beginning. + t.move(t.cursorY, 0, 0, 0) + t.cursorX, t.cursorY = 0, 0 + + t.queue(t.prompt) + t.advanceCursor(visualLength(t.prompt)) + t.writeLine(t.line) + t.moveCursorToPos(t.pos) +} + +func (t *Terminal) SetSize(width, height int) error { t.lock.Lock() defer t.lock.Unlock() + oldWidth := t.termWidth t.termWidth, t.termHeight = width, height + + switch { + case width == oldWidth || len(t.line) == 0: + // If the width didn't change then nothing else needs to be + // done. + return nil + case width < oldWidth: + // Some terminals (e.g. xterm) will truncate lines that were + // too long when shinking. Others, (e.g. gnome-terminal) will + // attempt to wrap them. For the former, repainting t.maxLine + // works great, but that behaviour goes badly wrong in the case + // of the latter because they have doubled every full line. + + // We assume that we are working on a terminal that wraps lines + // and adjust the cursor position based on every previous line + // wrapping and turning into two. This causes the prompt on + // xterms to move upwards, which isn't great, but it avoids a + // huge mess with gnome-terminal. + t.cursorY *= 2 + t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2) + case width > oldWidth: + // If the terminal expands then our position calculations will + // be wrong in the future because we think the cursor is + // |t.pos| chars into the string, but there will be a gap at + // the end of any wrapped line. + // + // But the position will actually be correct until we move, so + // we can move back to the beginning and repaint everything. + t.clearAndRepaintLinePlusNPrevious(t.maxLine) + } + + _, err := t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + return err } // stRingBuffer is a ring buffer of strings. From 2a0b1644c057a8d5bb0d4da04d143f12810d9631 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Sat, 25 Oct 2014 11:16:08 -0700 Subject: [PATCH 19/58] go.crypto/ssh/terminal: fix crash when terminal narrower than prompt. Previously, if the current line was "empty", resizes wouldn't trigger repaints. However, the line can be empty when the prompt is non-empty and the code would then panic after a resize because the cursor position was outside of the terminal. LGTM=bradfitz R=bradfitz CC=golang-codereviews https://golang.org/cl/158090043 --- terminal.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/terminal.go b/terminal.go index 123de5e..1ee1b44 100644 --- a/terminal.go +++ b/terminal.go @@ -732,11 +732,15 @@ func (t *Terminal) SetSize(width, height int) error { t.lock.Lock() defer t.lock.Unlock() + if width == 0 { + width = 1 + } + oldWidth := t.termWidth t.termWidth, t.termHeight = width, height switch { - case width == oldWidth || len(t.line) == 0: + case width == oldWidth: // If the width didn't change then nothing else needs to be // done. return nil @@ -752,6 +756,9 @@ func (t *Terminal) SetSize(width, height int) error { // wrapping and turning into two. This causes the prompt on // xterms to move upwards, which isn't great, but it avoids a // huge mess with gnome-terminal. + if t.cursorX >= t.termWidth { + t.cursorX = t.termWidth - 1 + } t.cursorY *= 2 t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2) case width > oldWidth: From e82ff8040c08f5521269ba7958ed780bc820df0f Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Tue, 4 Nov 2014 10:58:06 -0800 Subject: [PATCH 20/58] go.crypto/ssh/terminal: remove \r from passwords on Windows. Fixes golang/go#9040. (Note: can't compile or test this one prior to committing.) LGTM=iant, bradfitz R=bradfitz, mathias.gumz, iant CC=golang-codereviews https://golang.org/cl/171000043 --- util_windows.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/util_windows.go b/util_windows.go index 0a454e0..2dd6c3d 100644 --- a/util_windows.go +++ b/util_windows.go @@ -161,6 +161,9 @@ func ReadPassword(fd int) ([]byte, error) { if buf[n-1] == '\n' { n-- } + if n > 0 && buf[n-1] == '\r' { + n-- + } ret = append(ret, buf[:n]...) if n < len(buf) { break From 21bda07dd6156b48509432579919e331f61e03f8 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Fri, 7 Nov 2014 19:20:14 -0800 Subject: [PATCH 21/58] go.crypto/ssh/terminal: fix Home and End. In my notes I had Home and End down as OH and OF. But that's nonsense, they are [H and ]F. I never noticed before because I don't have Home and End keys on my keyboard. LGTM=bradfitz R=bradfitz CC=golang-codereviews https://golang.org/cl/172100043 --- terminal.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/terminal.go b/terminal.go index 1ee1b44..fd97611 100644 --- a/terminal.go +++ b/terminal.go @@ -166,11 +166,6 @@ func bytesToKey(b []byte) (rune, []byte) { return keyRight, b[3:] case 'D': return keyLeft, b[3:] - } - } - - if len(b) >= 3 && b[0] == keyEscape && b[1] == 'O' { - switch b[2] { case 'H': return keyHome, b[3:] case 'F': From d037c2cd545d7d66f7718b15566107e046267a62 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Sun, 16 Nov 2014 14:01:45 -0800 Subject: [PATCH 22/58] go.crypto/ssh/terminal: support bracketed paste mode. Some terminals support a mode where pasted text is bracketed by escape sequences. This is very useful for terminal applications that otherwise have no good way to tell pastes and typed text apart. This change allows applications to enable this mode and, if the terminal supports it, will suppress autocompletes during pastes and indicate to the caller that a line came entirely from pasted text. LGTM=bradfitz R=bradfitz CC=golang-codereviews https://golang.org/cl/171330043 --- terminal.go | 145 +++++++++++++++++++++++++++++++++++------------ terminal_test.go | 18 ++++++ 2 files changed, 128 insertions(+), 35 deletions(-) diff --git a/terminal.go b/terminal.go index fd97611..965f0cf 100644 --- a/terminal.go +++ b/terminal.go @@ -5,6 +5,7 @@ package terminal import ( + "bytes" "io" "sync" "unicode/utf8" @@ -61,6 +62,9 @@ type Terminal struct { pos int // echo is true if local echo is enabled echo bool + // pasteActive is true iff there is a bracketed paste operation in + // progress. + pasteActive bool // 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 @@ -124,28 +128,35 @@ const ( keyDeleteWord keyDeleteLine keyClearScreen + keyPasteStart + keyPasteEnd ) +var pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} +var pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} + // bytesToKey tries to parse a key sequence from b. If successful, it returns // the key and the remainder of the input. Otherwise it returns utf8.RuneError. -func bytesToKey(b []byte) (rune, []byte) { +func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { if len(b) == 0 { return utf8.RuneError, nil } - switch b[0] { - case 1: // ^A - return keyHome, b[1:] - case 5: // ^E - return keyEnd, b[1:] - case 8: // ^H - return keyBackspace, b[1:] - case 11: // ^K - return keyDeleteLine, b[1:] - case 12: // ^L - return keyClearScreen, b[1:] - case 23: // ^W - return keyDeleteWord, b[1:] + if !pasteActive { + switch b[0] { + case 1: // ^A + return keyHome, b[1:] + case 5: // ^E + return keyEnd, b[1:] + case 8: // ^H + return keyBackspace, b[1:] + case 11: // ^K + return keyDeleteLine, b[1:] + case 12: // ^L + return keyClearScreen, b[1:] + case 23: // ^W + return keyDeleteWord, b[1:] + } } if b[0] != keyEscape { @@ -156,7 +167,7 @@ func bytesToKey(b []byte) (rune, []byte) { return r, b[l:] } - if len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { + if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { switch b[2] { case 'A': return keyUp, b[3:] @@ -173,7 +184,7 @@ func bytesToKey(b []byte) (rune, []byte) { } } - if len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { + if !pasteActive && 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:] @@ -182,12 +193,20 @@ func bytesToKey(b []byte) (rune, []byte) { } } + if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) { + return keyPasteStart, b[6:] + } + + if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) { + return keyPasteEnd, 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 + // 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' { + if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' { return keyUnknown, b[i+1:] } } @@ -409,6 +428,11 @@ func visualLength(runes []rune) int { // handleKey processes the given key and, optionally, returns a line of text // that the user has entered. func (t *Terminal) handleKey(key rune) (line string, ok bool) { + if t.pasteActive && key != keyEnter { + t.addKeyToLine(key) + return + } + switch key { case keyBackspace: if t.pos == 0 { @@ -533,23 +557,29 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { if len(t.line) == maxLineLength { return } - if len(t.line) == cap(t.line) { - newLine := make([]rune, len(t.line), 2*(1+len(t.line))) - copy(newLine, t.line) - t.line = newLine - } - t.line = t.line[:len(t.line)+1] - copy(t.line[t.pos+1:], t.line[t.pos:]) - t.line[t.pos] = key - if t.echo { - t.writeLine(t.line[t.pos:]) - } - t.pos++ - t.moveCursorToPos(t.pos) + t.addKeyToLine(key) } return } +// addKeyToLine inserts the given key at the current position in the current +// line. +func (t *Terminal) addKeyToLine(key rune) { + if len(t.line) == cap(t.line) { + newLine := make([]rune, len(t.line), 2*(1+len(t.line))) + copy(newLine, t.line) + t.line = newLine + } + t.line = t.line[:len(t.line)+1] + copy(t.line[t.pos+1:], t.line[t.pos:]) + t.line[t.pos] = key + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.pos++ + t.moveCursorToPos(t.pos) +} + func (t *Terminal) writeLine(line []rune) { for len(line) != 0 { remainingOnLine := t.termWidth - t.cursorX @@ -643,19 +673,36 @@ func (t *Terminal) readLine() (line string, err error) { t.outBuf = t.outBuf[:0] } + lineIsPasted := t.pasteActive + for { rest := t.remainder lineOk := false for !lineOk { var key rune - key, rest = bytesToKey(rest) + key, rest = bytesToKey(rest, t.pasteActive) if key == utf8.RuneError { break } - if key == keyCtrlD { - if len(t.line) == 0 { - return "", io.EOF + if !t.pasteActive { + if key == keyCtrlD { + if len(t.line) == 0 { + return "", io.EOF + } } + if key == keyPasteStart { + t.pasteActive = true + if len(t.line) == 0 { + lineIsPasted = true + } + continue + } + } else if key == keyPasteEnd { + t.pasteActive = false + continue + } + if !t.pasteActive { + lineIsPasted = false } line, lineOk = t.handleKey(key) } @@ -672,6 +719,9 @@ func (t *Terminal) readLine() (line string, err error) { t.historyIndex = -1 t.history.Add(line) } + if lineIsPasted { + err = ErrPasteIndicator + } return } @@ -772,6 +822,31 @@ func (t *Terminal) SetSize(width, height int) error { return err } +type pasteIndicatorError struct{} + +func (pasteIndicatorError) Error() string { + return "terminal: ErrPasteIndicator not correctly handled" +} + +// ErrPasteIndicator may be returned from ReadLine as the error, in addition +// to valid line data. It indicates that bracketed paste mode is enabled and +// that the returned line consists only of pasted data. Programs may wish to +// interpret pasted data more literally than typed data. +var ErrPasteIndicator = pasteIndicatorError{} + +// SetBracketedPasteMode requests that the terminal bracket paste operations +// with markers. Not all terminals support this but, if it is supported, then +// enabling this mode will stop any autocomplete callback from running due to +// pastes. Additionally, any lines that are completely pasted will be returned +// from ReadLine with the error set to ErrPasteIndicator. +func (t *Terminal) SetBracketedPasteMode(on bool) { + if on { + io.WriteString(t.c, "\x1b[?2004h") + } else { + io.WriteString(t.c, "\x1b[?2004l") + } +} + // stRingBuffer is a ring buffer of strings. type stRingBuffer struct { // entries contains max elements. diff --git a/terminal_test.go b/terminal_test.go index fb42d76..6579801 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -179,6 +179,24 @@ var keyPressTests = []struct { in: "abcd\x1b[D\x1b[D\025\r", line: "cd", }, + { + // Bracketed paste mode: control sequences should be returned + // verbatim in paste mode. + in: "abc\x1b[200~de\177f\x1b[201~\177\r", + line: "abcde\177", + }, + { + // Enter in bracketed paste mode should still work. + in: "abc\x1b[200~d\refg\x1b[201~h\r", + line: "efgh", + throwAwayLines: 1, + }, + { + // Lines consisting entirely of pasted data should be indicated as such. + in: "\x1b[200~a\r", + line: "a", + err: ErrPasteIndicator, + }, } func TestKeyPresses(t *testing.T) { From 3c38914b08f12b27bff0ad2105b42ef4c65a9461 Mon Sep 17 00:00:00 2001 From: David Symonds Date: Tue, 9 Dec 2014 13:38:15 +1100 Subject: [PATCH 23/58] crypto: add import comments. Change-Id: I33240faf1b8620d0cd600de661928d8e422ebdbc Reviewed-on: https://go-review.googlesource.com/1235 Reviewed-by: Andrew Gerrand --- util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util.go b/util.go index 0763c9a..598e3df 100644 --- a/util.go +++ b/util.go @@ -14,7 +14,7 @@ // panic(err) // } // defer terminal.Restore(0, oldState) -package terminal +package terminal // import "golang.org/x/crypto/ssh/terminal" import ( "io" From fd5a52a5d8b7d9e40debc473d5799bfafb8c60db Mon Sep 17 00:00:00 2001 From: Derek Che Date: Thu, 18 Dec 2014 21:22:47 -0800 Subject: [PATCH 24/58] ssh/terminal: fix SetSize when nothing on current line SetSize has a problem may cause the following ReadPassword setting temporary prompt not working, when changing width the current SetSize will call clearAndRepaintLinePlusNPrevious which would print an old prompt whatever the current line has, causing a following ReadPassword with temporary prompt not printing the different prompt. When running code like this, the nt.SetSize prints a "> " as prompt then the temporary "Password: " prompt would never show up. ```go oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) width, height, _ = terminal.GetSize(int(os.Stdin.Fd())) nt := terminal.NewTerminal(os.Stdin, "> ") nt.SetSize(width, height) password, err = nt.ReadPassword("Password: ") ``` the new test cases is to test SetSize with different terminal sizes, either shrinking or expanding, a following ReadPassword should get the correct temporary prompt. Change-Id: I33d13b2c732997c0c88670d53545b8c0048b94b6 Reviewed-on: https://go-review.googlesource.com/1861 Reviewed-by: Adam Langley --- terminal.go | 4 ++++ terminal_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/terminal.go b/terminal.go index 965f0cf..741eeb1 100644 --- a/terminal.go +++ b/terminal.go @@ -789,6 +789,10 @@ func (t *Terminal) SetSize(width, height int) error { // If the width didn't change then nothing else needs to be // done. return nil + case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0: + // If there is nothing on current line and no prompt printed, + // just do nothing + return nil case width < oldWidth: // Some terminals (e.g. xterm) will truncate lines that were // too long when shinking. Others, (e.g. gnome-terminal) will diff --git a/terminal_test.go b/terminal_test.go index 6579801..a663fe4 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -241,3 +241,29 @@ func TestPasswordNotSaved(t *testing.T) { t.Fatalf("password was saved in history") } } + +var setSizeTests = []struct { + width, height int +}{ + {40, 13}, + {80, 24}, + {132, 43}, +} + +func TestTerminalSetSize(t *testing.T) { + for _, setSize := range setSizeTests { + c := &MockTerminal{ + toSend: []byte("password\r\x1b[A\r"), + bytesPerRead: 1, + } + ss := NewTerminal(c, "> ") + ss.SetSize(setSize.width, setSize.height) + pw, _ := ss.ReadPassword("Password: ") + if pw != "password" { + t.Fatalf("failed to read password, got %s", pw) + } + if string(c.received) != "Password: \r\n" { + t.Errorf("failed to set the temporary prompt expected %q, got %q", "Password: ", c.received) + } + } +} From caba55058b824d4d87e4d52960a6f1e012bc7a0a Mon Sep 17 00:00:00 2001 From: John Schnake Date: Fri, 8 Apr 2016 08:26:40 -0500 Subject: [PATCH 25/58] x/crypto/ssh/terminal: create stubs for plan9 methods To facilitate testing of methods in other GOOSs we need plan9 to be able to build and run the test without a errors due to undefined methods. Fixes golang/go#15195 Change-Id: Ida334676f92db6fb4652af3e3a9f6bc13a96052c Reviewed-on: https://go-review.googlesource.com/21711 Reviewed-by: Brad Fitzpatrick Run-TryBot: Brad Fitzpatrick --- util_plan9.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 util_plan9.go diff --git a/util_plan9.go b/util_plan9.go new file mode 100644 index 0000000..799f049 --- /dev/null +++ b/util_plan9.go @@ -0,0 +1,58 @@ +// Copyright 2016 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 terminal provides support functions for dealing with terminals, as +// commonly found on UNIX systems. +// +// Putting a terminal into raw mode is the most common requirement: +// +// oldState, err := terminal.MakeRaw(0) +// if err != nil { +// panic(err) +// } +// defer terminal.Restore(0, oldState) +package terminal + +import ( + "fmt" + "runtime" +) + +type State struct{} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + return false +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} From 896bd7dad42f4ec60d8a89185d160ebe3d5a0d34 Mon Sep 17 00:00:00 2001 From: John Schnake Date: Wed, 6 Apr 2016 11:11:13 -0500 Subject: [PATCH 26/58] x/crypto/ssh/terminal: ensure windows MakeRaw returns previous state The MakeRaw method should be returning the original state so that it can be restored. However, the current implementation is returning the new, "raw" state. Fixes golang/go#15155 Change-Id: I8e0b87229b7577544e1118fa4b95664d3a9cf5da Reviewed-on: https://go-review.googlesource.com/21612 Run-TryBot: Brad Fitzpatrick TryBot-Result: Gobot Gobot Reviewed-by: Brad Fitzpatrick --- terminal_test.go | 22 ++++++++++++++++++++++ util_windows.go | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/terminal_test.go b/terminal_test.go index a663fe4..6bdefb4 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -6,6 +6,7 @@ package terminal import ( "io" + "os" "testing" ) @@ -267,3 +268,24 @@ func TestTerminalSetSize(t *testing.T) { } } } + +func TestMakeRawState(t *testing.T) { + fd := int(os.Stdout.Fd()) + if !IsTerminal(fd) { + t.Skip("stdout is not a terminal; skipping test") + } + + st, err := GetState(fd) + if err != nil { + t.Fatalf("failed to get terminal state from GetState: %s", err) + } + defer Restore(fd, st) + raw, err := MakeRaw(fd) + if err != nil { + t.Fatalf("failed to get terminal state from MakeRaw: %s", err) + } + + if *st != *raw { + t.Errorf("states do not match; was %v, expected %v", raw, st) + } +} diff --git a/util_windows.go b/util_windows.go index 2dd6c3d..ae9fa9e 100644 --- a/util_windows.go +++ b/util_windows.go @@ -87,8 +87,8 @@ func MakeRaw(fd int) (*State, error) { if e != 0 { return nil, error(e) } - st &^= (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput) - _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0) + raw := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput) + _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(raw), 0) if e != 0 { return nil, error(e) } From 18e6eb769a62655dfabccb1029970a371ced1026 Mon Sep 17 00:00:00 2001 From: Faiyaz Ahmed Date: Mon, 9 May 2016 20:08:17 -0700 Subject: [PATCH 27/58] x/crypto/ssh/terminal: have MakeRaw mirror cfmakeraw. Rather than guessing at which terminal flags should be set or cleared by MakeRaw, this change tries to make it mirror the behaviour documented for cfmakeraw() in the termios(3) manpage. Fixes golang/go#15625 Change-Id: Icd6b18ffb57ea332147c8c9b25eac5e41eb0863a Reviewed-on: https://go-review.googlesource.com/22964 Run-TryBot: Adam Langley TryBot-Result: Gobot Gobot Reviewed-by: Faiyaz Ahmed Reviewed-by: Adam Langley --- util.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/util.go b/util.go index 598e3df..c869213 100644 --- a/util.go +++ b/util.go @@ -44,8 +44,13 @@ func MakeRaw(fd int) (*State, error) { } newState := oldState.termios - newState.Iflag &^= syscall.ISTRIP | syscall.INLCR | syscall.ICRNL | syscall.IGNCR | syscall.IXON | syscall.IXOFF - newState.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG + // This attempts to replicate the behaviour documented for cfmakeraw in + // the termios(3) manpage. + newState.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON + newState.Oflag &^= syscall.OPOST + newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN + newState.Cflag &^= syscall.CSIZE | syscall.PARENB + newState.Cflag |= syscall.CS8 if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { return nil, err } From d7ffdd5ca2648e10b13e00608592c294dc5f3e9b Mon Sep 17 00:00:00 2001 From: Fazal Majid Date: Mon, 2 Nov 2015 13:41:26 -0800 Subject: [PATCH 28/58] ssh/terminal: implement ReadPassword and IsTerminal Fixes golang/go#13085. Change-Id: I2fcdd60e5e8db032d6fa3ce76198bdc7a63f3cf6 Reviewed-on: https://go-review.googlesource.com/16722 Run-TryBot: Russ Cox Reviewed-by: Russ Cox --- util_solaris.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 util_solaris.go diff --git a/util_solaris.go b/util_solaris.go new file mode 100644 index 0000000..07eb5ed --- /dev/null +++ b/util_solaris.go @@ -0,0 +1,73 @@ +// Copyright 2015 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. + +// +build solaris + +package terminal // import "golang.org/x/crypto/ssh/terminal" + +import ( + "golang.org/x/sys/unix" + "io" + "syscall" +) + +// State contains the state of a terminal. +type State struct { + termios syscall.Termios +} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + // see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c + var termio unix.Termio + err := unix.IoctlSetTermio(fd, unix.TCGETA, &termio) + return err == nil +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + // see also: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libast/common/uwin/getpass.c + val, err := unix.IoctlGetTermios(fd, unix.TCGETS) + if err != nil { + return nil, err + } + oldState := *val + + newState := oldState + newState.Lflag &^= syscall.ECHO + newState.Lflag |= syscall.ICANON | syscall.ISIG + newState.Iflag |= syscall.ICRNL + err = unix.IoctlSetTermios(fd, unix.TCSETS, &newState) + if err != nil { + return nil, err + } + + defer unix.IoctlSetTermios(fd, unix.TCSETS, &oldState) + + var buf [16]byte + var ret []byte + for { + n, err := syscall.Read(fd, buf[:]) + if err != nil { + return nil, err + } + if n == 0 { + if len(ret) == 0 { + return nil, io.EOF + } + break + } + if buf[n-1] == '\n' { + n-- + } + ret = append(ret, buf[:n]...) + if n < len(buf) { + break + } + } + + return ret, nil +} From 3ea8c23763c06939d889ab508838bf0a91bfbfc5 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Mon, 5 Dec 2016 09:23:45 -0800 Subject: [PATCH 29/58] x/crypto/ssh/terminal: replace \n with \r\n. 18e6eb769a6 made MakeRaw match C's behaviour. This included clearing the OPOST flag, which means that one now needs to write \r\n for a newline, otherwise the cursor doesn't move back to the beginning and the terminal prints a staircase. (Dear god, we're still emulating line printers.) This change causes the terminal package to do the required transformation. Fixes golang/go#17364. Change-Id: Ida15d3cf701a21eaa59161ab61b3ed4dee2ded46 Reviewed-on: https://go-review.googlesource.com/33902 Reviewed-by: Brad Fitzpatrick --- terminal.go | 42 +++++++++++++++++++++++++++++++++++++----- terminal_test.go | 15 +++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/terminal.go b/terminal.go index 741eeb1..5ea89a2 100644 --- a/terminal.go +++ b/terminal.go @@ -132,8 +132,11 @@ const ( keyPasteEnd ) -var pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} -var pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} +var ( + crlf = []byte{'\r', '\n'} + pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} + pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} +) // bytesToKey tries to parse a key sequence from b. If successful, it returns // the key and the remainder of the input. Otherwise it returns utf8.RuneError. @@ -333,7 +336,7 @@ func (t *Terminal) advanceCursor(places int) { // So, if we are stopping at the end of a line, we // need to write a newline so that our cursor can be // advanced to the next line. - t.outBuf = append(t.outBuf, '\n') + t.outBuf = append(t.outBuf, '\r', '\n') } } @@ -593,6 +596,35 @@ func (t *Terminal) writeLine(line []rune) { } } +// writeWithCRLF writes buf to w but replaces all occurances of \n with \r\n. +func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) { + for len(buf) > 0 { + i := bytes.IndexByte(buf, '\n') + todo := len(buf) + if i >= 0 { + todo = i + } + + var nn int + nn, err = w.Write(buf[:todo]) + n += nn + if err != nil { + return n, err + } + buf = buf[todo:] + + if i >= 0 { + if _, err = w.Write(crlf); err != nil { + return n, err + } + n += 1 + buf = buf[1:] + } + } + + return n, nil +} + func (t *Terminal) Write(buf []byte) (n int, err error) { t.lock.Lock() defer t.lock.Unlock() @@ -600,7 +632,7 @@ func (t *Terminal) Write(buf []byte) (n int, err error) { if t.cursorX == 0 && t.cursorY == 0 { // This is the easy case: there's nothing on the screen that we // have to move out of the way. - return t.c.Write(buf) + return writeWithCRLF(t.c, buf) } // We have a prompt and possibly user input on the screen. We @@ -620,7 +652,7 @@ func (t *Terminal) Write(buf []byte) (n int, err error) { } t.outBuf = t.outBuf[:0] - if n, err = t.c.Write(buf); err != nil { + if n, err = writeWithCRLF(t.c, buf); err != nil { return } diff --git a/terminal_test.go b/terminal_test.go index 6bdefb4..1d54c4f 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -5,6 +5,7 @@ package terminal import ( + "bytes" "io" "os" "testing" @@ -289,3 +290,17 @@ func TestMakeRawState(t *testing.T) { t.Errorf("states do not match; was %v, expected %v", raw, st) } } + +func TestOutputNewlines(t *testing.T) { + // \n should be changed to \r\n in terminal output. + buf := new(bytes.Buffer) + term := NewTerminal(buf, ">") + + term.Write([]byte("1\n2\n")) + output := string(buf.Bytes()) + const expected = "1\r\n2\r\n" + + if output != expected { + t.Errorf("incorrect output: was %q, expected %q", output, expected) + } +} From 426ef97d77f2e267f814774031e63ae12bc88459 Mon Sep 17 00:00:00 2001 From: Mikio Hara Date: Mon, 19 Dec 2016 06:06:48 +0900 Subject: [PATCH 30/58] ssh/terminal: fix a typo Change-Id: Iafe2ebb6d37afd2a64aa72750a722d4860bb735e Reviewed-on: https://go-review.googlesource.com/34535 Run-TryBot: Mikio Hara TryBot-Result: Gobot Gobot Reviewed-by: Brad Fitzpatrick --- terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.go b/terminal.go index 5ea89a2..f816773 100644 --- a/terminal.go +++ b/terminal.go @@ -596,7 +596,7 @@ func (t *Terminal) writeLine(line []rune) { } } -// writeWithCRLF writes buf to w but replaces all occurances of \n with \r\n. +// writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n. func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) { for len(buf) > 0 { i := bytes.IndexByte(buf, '\n') From 3acf1ca968e48d038b2b5ac02cadbcb283bfcb55 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 4 Jan 2017 14:40:51 -0800 Subject: [PATCH 31/58] all: fix some vet warnings Change-Id: I85c2912a6862c6c251450f2a0926ecd33a9fb8e7 Reviewed-on: https://go-review.googlesource.com/34815 Reviewed-by: Brad Fitzpatrick Run-TryBot: Brad Fitzpatrick TryBot-Result: Gobot Gobot --- terminal.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/terminal.go b/terminal.go index f816773..0c5bd56 100644 --- a/terminal.go +++ b/terminal.go @@ -772,8 +772,6 @@ func (t *Terminal) readLine() (line string, err error) { t.remainder = t.inBuf[:n+len(t.remainder)] } - - panic("unreachable") // for Go 1.0. } // SetPrompt sets the prompt to be used when reading subsequent lines. From a5e02a7a6f743c9b5f7cc83322d4e31288c1a801 Mon Sep 17 00:00:00 2001 From: Peter Morjan Date: Sat, 7 Jan 2017 15:41:52 +0100 Subject: [PATCH 32/58] ssh/terminal: consistent return value for Restore This patch makes the Restore function return nil on success to be consistent with other functions like MakeRaw. Change-Id: I81e63f568787dd88466a5bb30cb87c4c3be75a5c Reviewed-on: https://go-review.googlesource.com/34952 Reviewed-by: Brad Fitzpatrick Run-TryBot: Brad Fitzpatrick TryBot-Result: Gobot Gobot --- util.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/util.go b/util.go index c869213..747f1b8 100644 --- a/util.go +++ b/util.go @@ -72,8 +72,10 @@ func GetState(fd int) (*State, error) { // Restore restores the terminal connected to the given file descriptor to a // previous state. func Restore(fd int, state *State) error { - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) - return err + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0); err != 0 { + return err + } + return nil } // GetSize returns the dimensions of the given terminal. From 038bd0bb6d7b20ac179a3ba09572afb6cca3c43c Mon Sep 17 00:00:00 2001 From: Alex Brainman Date: Tue, 2 Aug 2016 12:34:46 +1000 Subject: [PATCH 33/58] ssh/terminal: fix line endings handling in ReadPassword Fixes golang/go#16552 Change-Id: I18a9c9b42fe042c4871b3efb3f51bef7cca335d0 Reviewed-on: https://go-review.googlesource.com/25355 Reviewed-by: Adam Langley Reviewed-by: Adam Langley --- terminal.go | 28 ++++++++++++++++++++++++++++ terminal_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ util.go | 32 ++++++++------------------------ util_windows.go | 35 ++++++++--------------------------- 4 files changed, 88 insertions(+), 51 deletions(-) diff --git a/terminal.go b/terminal.go index 0c5bd56..d35e8b1 100644 --- a/terminal.go +++ b/terminal.go @@ -920,3 +920,31 @@ func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { } return s.entries[index], true } + +// readPasswordLine reads from reader until it finds \n or io.EOF. +// The slice returned does not include the \n. +// readPasswordLine also ignores any \r it finds. +func readPasswordLine(reader io.Reader) ([]byte, error) { + var buf [1]byte + var ret []byte + + for { + n, err := reader.Read(buf[:]) + if err != nil { + if err == io.EOF && len(ret) > 0 { + return ret, nil + } + return ret, err + } + if n > 0 { + switch buf[0] { + case '\n': + return ret, nil + case '\r': + // remove \r from passwords on Windows + default: + ret = append(ret, buf[0]) + } + } + } +} diff --git a/terminal_test.go b/terminal_test.go index 1d54c4f..901c72a 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -270,6 +270,50 @@ func TestTerminalSetSize(t *testing.T) { } } +func TestReadPasswordLineEnd(t *testing.T) { + var tests = []struct { + input string + want string + }{ + {"\n", ""}, + {"\r\n", ""}, + {"test\r\n", "test"}, + {"testtesttesttes\n", "testtesttesttes"}, + {"testtesttesttes\r\n", "testtesttesttes"}, + {"testtesttesttesttest\n", "testtesttesttesttest"}, + {"testtesttesttesttest\r\n", "testtesttesttesttest"}, + } + for _, test := range tests { + buf := new(bytes.Buffer) + if _, err := buf.WriteString(test.input); err != nil { + t.Fatal(err) + } + + have, err := readPasswordLine(buf) + if err != nil { + t.Errorf("readPasswordLine(%q) failed: %v", test.input, err) + continue + } + if string(have) != test.want { + t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want) + continue + } + + if _, err = buf.WriteString(test.input); err != nil { + t.Fatal(err) + } + have, err = readPasswordLine(buf) + if err != nil { + t.Errorf("readPasswordLine(%q) failed: %v", test.input, err) + continue + } + if string(have) != test.want { + t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want) + continue + } + } +} + func TestMakeRawState(t *testing.T) { fd := int(os.Stdout.Fd()) if !IsTerminal(fd) { diff --git a/util.go b/util.go index 747f1b8..d019196 100644 --- a/util.go +++ b/util.go @@ -17,7 +17,6 @@ package terminal // import "golang.org/x/crypto/ssh/terminal" import ( - "io" "syscall" "unsafe" ) @@ -88,6 +87,13 @@ func GetSize(fd int) (width, height int, err error) { return int(dimensions[1]), int(dimensions[0]), nil } +// passwordReader is an io.Reader that reads from a specific file descriptor. +type passwordReader int + +func (r passwordReader) Read(buf []byte) (int, error) { + return syscall.Read(int(r), buf) +} + // ReadPassword reads a line of input from a terminal without local echo. This // is commonly used for inputting passwords and other sensitive data. The slice // returned does not include the \n. @@ -109,27 +115,5 @@ func ReadPassword(fd int) ([]byte, error) { syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) }() - var buf [16]byte - var ret []byte - for { - n, err := syscall.Read(fd, buf[:]) - if err != nil { - return nil, err - } - if n == 0 { - if len(ret) == 0 { - return nil, io.EOF - } - break - } - if buf[n-1] == '\n' { - n-- - } - ret = append(ret, buf[:n]...) - if n < len(buf) { - break - } - } - - return ret, nil + return readPasswordLine(passwordReader(fd)) } diff --git a/util_windows.go b/util_windows.go index ae9fa9e..e0a1f36 100644 --- a/util_windows.go +++ b/util_windows.go @@ -17,7 +17,6 @@ package terminal import ( - "io" "syscall" "unsafe" ) @@ -123,6 +122,13 @@ func GetSize(fd int) (width, height int, err error) { return int(info.size.x), int(info.size.y), nil } +// passwordReader is an io.Reader that reads from a specific Windows HANDLE. +type passwordReader int + +func (r passwordReader) Read(buf []byte) (int, error) { + return syscall.Read(syscall.Handle(r), buf) +} + // ReadPassword reads a line of input from a terminal without local echo. This // is commonly used for inputting passwords and other sensitive data. The slice // returned does not include the \n. @@ -145,30 +151,5 @@ func ReadPassword(fd int) ([]byte, error) { syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0) }() - var buf [16]byte - var ret []byte - for { - n, err := syscall.Read(syscall.Handle(fd), buf[:]) - if err != nil { - return nil, err - } - if n == 0 { - if len(ret) == 0 { - return nil, io.EOF - } - break - } - if buf[n-1] == '\n' { - n-- - } - if n > 0 && buf[n-1] == '\r' { - n-- - } - ret = append(ret, buf[:n]...) - if n < len(buf) { - break - } - } - - return ret, nil + return readPasswordLine(passwordReader(fd)) } From efcab1a52eda5485653d7d1b4c8678d930667dec Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Mon, 16 Jan 2017 16:40:45 -0800 Subject: [PATCH 34/58] ssh/terminal: consume data before checking for an error. According to the io.Reader docs, Alex had it right the first time. (See discussion on https://golang.org/cl/25355.) Change-Id: Ib6fb9dfb99009e034263574e82d7e9d4828df38f Reviewed-on: https://go-review.googlesource.com/35242 TryBot-Result: Gobot Gobot Run-TryBot: Adam Langley Reviewed-by: Alex Brainman Reviewed-by: Adam Langley --- terminal.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/terminal.go b/terminal.go index d35e8b1..18379a9 100644 --- a/terminal.go +++ b/terminal.go @@ -930,12 +930,6 @@ func readPasswordLine(reader io.Reader) ([]byte, error) { for { n, err := reader.Read(buf[:]) - if err != nil { - if err == io.EOF && len(ret) > 0 { - return ret, nil - } - return ret, err - } if n > 0 { switch buf[0] { case '\n': @@ -945,6 +939,13 @@ func readPasswordLine(reader io.Reader) ([]byte, error) { default: ret = append(ret, buf[0]) } + continue + } + if err != nil { + if err == io.EOF && len(ret) > 0 { + return ret, nil + } + return ret, err } } } From 9d2f68fd6d719fe0c63e8dee5c008931547679e9 Mon Sep 17 00:00:00 2001 From: Rick Sayre Date: Thu, 20 Apr 2017 21:26:53 -0700 Subject: [PATCH 35/58] ssh/terminal: implement missing functions for Solaris/OmniOS terminal.MakeRaw terminal.Restore terminal.GetState terminal.GetSize Fixes golang/go#20062 Change-Id: I9ccf194215998c5b80dbedc4f248b481f0ca57a6 Reviewed-on: https://go-review.googlesource.com/41297 Reviewed-by: Brad Fitzpatrick --- util_solaris.go | 63 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/util_solaris.go b/util_solaris.go index 07eb5ed..a2e1b57 100644 --- a/util_solaris.go +++ b/util_solaris.go @@ -14,14 +14,12 @@ import ( // State contains the state of a terminal. type State struct { - termios syscall.Termios + state *unix.Termios } // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd int) bool { - // see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c - var termio unix.Termio - err := unix.IoctlSetTermio(fd, unix.TCGETA, &termio) + _, err := unix.IoctlGetTermio(fd, unix.TCGETA) return err == nil } @@ -71,3 +69,60 @@ func ReadPassword(fd int) ([]byte, error) { return ret, nil } + +// MakeRaw puts the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +// see http://cr.illumos.org/~webrev/andy_js/1060/ +func MakeRaw(fd int) (*State, error) { + oldTermiosPtr, err := unix.IoctlGetTermios(fd, unix.TCGETS) + if err != nil { + return nil, err + } + oldTermios := *oldTermiosPtr + + newTermios := oldTermios + newTermios.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON + newTermios.Oflag &^= syscall.OPOST + newTermios.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN + newTermios.Cflag &^= syscall.CSIZE | syscall.PARENB + newTermios.Cflag |= syscall.CS8 + newTermios.Cc[unix.VMIN] = 1 + newTermios.Cc[unix.VTIME] = 0 + + if err := unix.IoctlSetTermios(fd, unix.TCSETS, &newTermios); err != nil { + return nil, err + } + + return &State{ + state: oldTermiosPtr, + }, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, oldState *State) error { + return unix.IoctlSetTermios(fd, unix.TCSETS, oldState.state) +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + oldTermiosPtr, err := unix.IoctlGetTermios(fd, unix.TCGETS) + if err != nil { + return nil, err + } + + return &State{ + state: oldTermiosPtr, + }, nil +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return 0, 0, err + } + return int(ws.Col), int(ws.Row), nil +} From d45e3a0228469f9aed37d339c432109ec8f14347 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Fri, 28 Jul 2017 14:36:07 +0200 Subject: [PATCH 36/58] ssh/terminal: use termios ioctl read/write constants from x/sys/unix Use the TCGETS/TCSETS and TIOCGETA/TIOCSETA definitions from x/sys/unix instead of manually declaring them or using the corresponding definitions from syscall. Change-Id: I37c2c8124d251eb47467b4184a7cc39781775f11 Reviewed-on: https://go-review.googlesource.com/51690 Reviewed-by: Matt Layher Reviewed-by: Brad Fitzpatrick Run-TryBot: Matt Layher TryBot-Result: Gobot Gobot --- util_bsd.go | 6 +++--- util_linux.go | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/util_bsd.go b/util_bsd.go index 9c1ffd1..cb23a59 100644 --- a/util_bsd.go +++ b/util_bsd.go @@ -6,7 +6,7 @@ package terminal -import "syscall" +import "golang.org/x/sys/unix" -const ioctlReadTermios = syscall.TIOCGETA -const ioctlWriteTermios = syscall.TIOCSETA +const ioctlReadTermios = unix.TIOCGETA +const ioctlWriteTermios = unix.TIOCSETA diff --git a/util_linux.go b/util_linux.go index 5883b22..5fadfe8 100644 --- a/util_linux.go +++ b/util_linux.go @@ -4,8 +4,7 @@ package terminal -// These constants are declared here, rather than importing -// them from the syscall package as some syscall packages, even -// on linux, for example gccgo, do not declare them. -const ioctlReadTermios = 0x5401 // syscall.TCGETS -const ioctlWriteTermios = 0x5402 // syscall.TCSETS +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS +const ioctlWriteTermios = unix.TCSETS From 073b14d8cc7c71f7890d0783a5d8003fe4b9e0a4 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Thu, 3 Aug 2017 16:09:35 +0200 Subject: [PATCH 37/58] ssh/terminal: use console functions, types and consts from x/sys/windows Use GetConsoleMode, SetConsoleMode and GetConsoleScreenBufferInfo and the corresponding types and constants from golang.org/x/sys/windows instead of locally defining them. Change-Id: I69292a47114d071be261ffda6ca620a0b9820d00 Reviewed-on: https://go-review.googlesource.com/52990 TryBot-Result: Gobot Gobot Reviewed-by: Alex Brainman --- util_windows.go | 99 ++++++++++++------------------------------------- 1 file changed, 23 insertions(+), 76 deletions(-) diff --git a/util_windows.go b/util_windows.go index e0a1f36..60979cc 100644 --- a/util_windows.go +++ b/util_windows.go @@ -17,53 +17,7 @@ package terminal import ( - "syscall" - "unsafe" -) - -const ( - enableLineInput = 2 - enableEchoInput = 4 - enableProcessedInput = 1 - enableWindowInput = 8 - enableMouseInput = 16 - enableInsertMode = 32 - enableQuickEditMode = 64 - enableExtendedFlags = 128 - enableAutoPosition = 256 - enableProcessedOutput = 1 - enableWrapAtEolOutput = 2 -) - -var kernel32 = syscall.NewLazyDLL("kernel32.dll") - -var ( - procGetConsoleMode = kernel32.NewProc("GetConsoleMode") - procSetConsoleMode = kernel32.NewProc("SetConsoleMode") - procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") -) - -type ( - short int16 - word uint16 - - coord struct { - x short - y short - } - smallRect struct { - left short - top short - right short - bottom short - } - consoleScreenBufferInfo struct { - size coord - cursorPosition coord - attributes word - window smallRect - maximumWindowSize coord - } + "golang.org/x/sys/windows" ) type State struct { @@ -73,8 +27,8 @@ type State struct { // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd int) bool { var st uint32 - r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) - return r != 0 && e == 0 + err := windows.GetConsoleMode(windows.Handle(fd), &st) + return err == nil } // MakeRaw put the terminal connected to the given file descriptor into raw @@ -82,14 +36,12 @@ func IsTerminal(fd int) bool { // restored. func MakeRaw(fd int) (*State, error) { var st uint32 - _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) - if e != 0 { - return nil, error(e) + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err } - raw := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput) - _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(raw), 0) - if e != 0 { - return nil, error(e) + raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { + return nil, err } return &State{st}, nil } @@ -98,9 +50,8 @@ func MakeRaw(fd int) (*State, error) { // restore the terminal after a signal. func GetState(fd int) (*State, error) { var st uint32 - _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) - if e != 0 { - return nil, error(e) + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err } return &State{st}, nil } @@ -108,25 +59,23 @@ func GetState(fd int) (*State, error) { // Restore restores the terminal connected to the given file descriptor to a // previous state. func Restore(fd int, state *State) error { - _, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0) - return err + return windows.SetConsoleMode(windows.Handle(fd), state.mode) } // GetSize returns the dimensions of the given terminal. func GetSize(fd int) (width, height int, err error) { - var info consoleScreenBufferInfo - _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0) - if e != 0 { - return 0, 0, error(e) + var info windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { + return 0, 0, err } - return int(info.size.x), int(info.size.y), nil + return int(info.Size.X), int(info.Size.Y), nil } // passwordReader is an io.Reader that reads from a specific Windows HANDLE. type passwordReader int func (r passwordReader) Read(buf []byte) (int, error) { - return syscall.Read(syscall.Handle(r), buf) + return windows.Read(windows.Handle(r), buf) } // ReadPassword reads a line of input from a terminal without local echo. This @@ -134,21 +83,19 @@ func (r passwordReader) Read(buf []byte) (int, error) { // returned does not include the \n. func ReadPassword(fd int) ([]byte, error) { var st uint32 - _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) - if e != 0 { - return nil, error(e) + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err } old := st - st &^= (enableEchoInput) - st |= (enableProcessedInput | enableLineInput | enableProcessedOutput) - _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0) - if e != 0 { - return nil, error(e) + st &^= (windows.ENABLE_ECHO_INPUT) + st |= (windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil { + return nil, err } defer func() { - syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0) + windows.SetConsoleMode(windows.Handle(fd), old) }() return readPasswordLine(passwordReader(fd)) From 5cd87de7cccf6e7ec10bef8cb3dbfc472de3b52c Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Mon, 7 Aug 2017 12:11:13 +0200 Subject: [PATCH 38/58] ssh/terminal: set termios VMIN and VTIME in MakeRaw The Solaris version of MakeRaw already sets VMIN and VTIME explicitly such that a read returns when one character is available. cfmakeraw (whose behavior MakeRaw replicate) in glibc and the BSD's libc also set these values explicitly, so it should be done in the Linux/BSD versions of MakeRaw as well to be consistent. Change-Id: I531641ec87fd6a21b7a544b9a464bb90045b0bb1 Reviewed-on: https://go-review.googlesource.com/53570 Run-TryBot: Ian Lance Taylor TryBot-Result: Gobot Gobot Reviewed-by: Avelino Reviewed-by: Ian Lance Taylor --- util.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/util.go b/util.go index d019196..e7404ff 100644 --- a/util.go +++ b/util.go @@ -19,6 +19,8 @@ package terminal // import "golang.org/x/crypto/ssh/terminal" import ( "syscall" "unsafe" + + "golang.org/x/sys/unix" ) // State contains the state of a terminal. @@ -50,6 +52,8 @@ func MakeRaw(fd int) (*State, error) { newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN newState.Cflag &^= syscall.CSIZE | syscall.PARENB newState.Cflag |= syscall.CS8 + newState.Cc[unix.VMIN] = 1 + newState.Cc[unix.VTIME] = 0 if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { return nil, err } From a1a083ff5a00a76e37065c806c0d330fc2f7979b Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Sun, 5 Nov 2017 16:18:38 +0100 Subject: [PATCH 39/58] terminal/ssh: use ioctl wrappers from x/sys/unix Use the ioctl wrapper functions from x/sys/unix instead of manually re-implementing them. Change-Id: I224de0c6ec7439dfd8c45c72071c947be8813d6a Reviewed-on: https://go-review.googlesource.com/75991 Run-TryBot: Brad Fitzpatrick TryBot-Result: Gobot Gobot Reviewed-by: Brad Fitzpatrick --- util.go | 69 ++++++++++++++++++++++++++------------------------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/util.go b/util.go index e7404ff..02dad48 100644 --- a/util.go +++ b/util.go @@ -17,44 +17,41 @@ package terminal // import "golang.org/x/crypto/ssh/terminal" import ( - "syscall" - "unsafe" - "golang.org/x/sys/unix" ) // State contains the state of a terminal. type State struct { - termios syscall.Termios + termios unix.Termios } // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd int) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 + _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + return err == nil } // MakeRaw put the terminal connected to the given file descriptor into raw // mode and returns the previous state of the terminal so that it can be // restored. func MakeRaw(fd int) (*State, error) { - var oldState State - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { return nil, err } - newState := oldState.termios + oldState := State{termios: *termios} + // This attempts to replicate the behaviour documented for cfmakeraw in // the termios(3) manpage. - newState.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON - newState.Oflag &^= syscall.OPOST - newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN - newState.Cflag &^= syscall.CSIZE | syscall.PARENB - newState.Cflag |= syscall.CS8 - newState.Cc[unix.VMIN] = 1 - newState.Cc[unix.VTIME] = 0 - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + termios.Oflag &^= unix.OPOST + termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + termios.Cflag &^= unix.CSIZE | unix.PARENB + termios.Cflag |= unix.CS8 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { return nil, err } @@ -64,59 +61,55 @@ func MakeRaw(fd int) (*State, error) { // GetState returns the current state of a terminal which may be useful to // restore the terminal after a signal. func GetState(fd int) (*State, error) { - var oldState State - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { return nil, err } - return &oldState, nil + return &State{termios: *termios}, nil } // Restore restores the terminal connected to the given file descriptor to a // previous state. func Restore(fd int, state *State) error { - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0); err != 0 { - return err - } - return nil + return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) } // GetSize returns the dimensions of the given terminal. func GetSize(fd int) (width, height int, err error) { - var dimensions [4]uint16 - - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 { + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { return -1, -1, err } - return int(dimensions[1]), int(dimensions[0]), nil + return int(ws.Col), int(ws.Row), nil } // passwordReader is an io.Reader that reads from a specific file descriptor. type passwordReader int func (r passwordReader) Read(buf []byte) (int, error) { - return syscall.Read(int(r), buf) + return unix.Read(int(r), buf) } // ReadPassword reads a line of input from a terminal without local echo. This // is commonly used for inputting passwords and other sensitive data. The slice // returned does not include the \n. func ReadPassword(fd int) ([]byte, error) { - var oldState syscall.Termios - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { return nil, err } - newState := oldState - newState.Lflag &^= syscall.ECHO - newState.Lflag |= syscall.ICANON | syscall.ISIG - newState.Iflag |= syscall.ICRNL - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + newState := *termios + newState.Lflag &^= unix.ECHO + newState.Lflag |= unix.ICANON | unix.ISIG + newState.Iflag |= unix.ICRNL + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { return nil, err } defer func() { - syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) + unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) }() return readPasswordLine(passwordReader(fd)) From a21c5dde4c267671d53b40583e43e936a684a466 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 22 Nov 2017 18:46:40 +0900 Subject: [PATCH 40/58] ssh/terminal: handle non-ASCII characters when reading passwords ReadPassword uses Windows ReadFile to read from console handle. But ReadFile does not split input into UTF-8 characters, so ReadFile only works when input is ASCII. Use os.File instead of Windows ReadFile, because os.File reads console and parses it into UTF-8. Fixes golang/go#22828 Change-Id: Ifeed3e8048b51f46706c28d4154a3e4b10111a3e Reviewed-on: https://go-review.googlesource.com/79335 Reviewed-by: Alex Brainman Run-TryBot: Alex Brainman TryBot-Result: Gobot Gobot --- util_windows.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/util_windows.go b/util_windows.go index 60979cc..92944f3 100644 --- a/util_windows.go +++ b/util_windows.go @@ -17,6 +17,8 @@ package terminal import ( + "os" + "golang.org/x/sys/windows" ) @@ -71,13 +73,6 @@ func GetSize(fd int) (width, height int, err error) { return int(info.Size.X), int(info.Size.Y), nil } -// passwordReader is an io.Reader that reads from a specific Windows HANDLE. -type passwordReader int - -func (r passwordReader) Read(buf []byte) (int, error) { - return windows.Read(windows.Handle(r), buf) -} - // ReadPassword reads a line of input from a terminal without local echo. This // is commonly used for inputting passwords and other sensitive data. The slice // returned does not include the \n. @@ -98,5 +93,5 @@ func ReadPassword(fd int) ([]byte, error) { windows.SetConsoleMode(windows.Handle(fd), old) }() - return readPasswordLine(passwordReader(fd)) + return readPasswordLine(os.NewFile(uintptr(fd), "stdin")) } From f344d0325d325b8614653a12bb045ff8f12efdd9 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Mon, 27 Nov 2017 20:39:32 -0800 Subject: [PATCH 41/58] all: fix errors reported by vet, golint None are "wrong" per se, but there are a lot of good suggestions and in one case a docstring that was not present in godoc due to the presence of an extra newline. Changed "Id" in struct properties to "ID" in some non-exported structs. Removed a trailing period from some error messages; I believe the exact contents of error strings are not covered by the Go compatibility promise. Change-Id: I7c620582dc247396f72c52d38c909ccc0ec87b83 Reviewed-on: https://go-review.googlesource.com/80145 Run-TryBot: Brad Fitzpatrick TryBot-Result: Gobot Gobot Reviewed-by: Brad Fitzpatrick --- terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.go b/terminal.go index 18379a9..9a88759 100644 --- a/terminal.go +++ b/terminal.go @@ -617,7 +617,7 @@ func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) { if _, err = w.Write(crlf); err != nil { return n, err } - n += 1 + n++ buf = buf[1:] } } From 189f313d0caebef915a85507ed977b82d46bc2df Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 24 Jan 2018 12:37:23 +0900 Subject: [PATCH 42/58] ssh/terminal: use duplicate handle in ReadPassword os.NewFile assigns finalizer to close file handle passed into ReadPassword. But that is not expected. Make a duplicate of original file handle, and pass copy handle into ReadPassword instead. Fixes golang/go#23525 Change-Id: I4d6725e9a1cc20defd1b58afc383e35a7f9ee4e9 Reviewed-on: https://go-review.googlesource.com/89395 Reviewed-by: Alex Brainman Run-TryBot: Alex Brainman TryBot-Result: Gobot Gobot --- util_windows.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/util_windows.go b/util_windows.go index 92944f3..4933ac3 100644 --- a/util_windows.go +++ b/util_windows.go @@ -93,5 +93,13 @@ func ReadPassword(fd int) ([]byte, error) { windows.SetConsoleMode(windows.Handle(fd), old) }() - return readPasswordLine(os.NewFile(uintptr(fd), "stdin")) + var h windows.Handle + p, _ := windows.GetCurrentProcess() + if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { + return nil, err + } + + f := os.NewFile(uintptr(h), "stdin") + defer f.Close() + return readPasswordLine(f) } From f9bf865cb0d489dfebe9abc98849070791e9ffa3 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Wed, 7 Mar 2018 16:05:27 +0100 Subject: [PATCH 43/58] ssh/terminal: simplify defer Directly use unix.IoctlSetTermios and windows.SetConsoleMode instead of wrapping them with a closure. Change-Id: I6309253fbb6e59e029424273b48aaa608873ea17 Reviewed-on: https://go-review.googlesource.com/99455 Run-TryBot: Tobias Klauser TryBot-Result: Gobot Gobot Reviewed-by: Ian Lance Taylor --- util.go | 4 +--- util_windows.go | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/util.go b/util.go index 02dad48..731c89a 100644 --- a/util.go +++ b/util.go @@ -108,9 +108,7 @@ func ReadPassword(fd int) ([]byte, error) { return nil, err } - defer func() { - unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) - }() + defer unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) return readPasswordLine(passwordReader(fd)) } diff --git a/util_windows.go b/util_windows.go index 4933ac3..8618955 100644 --- a/util_windows.go +++ b/util_windows.go @@ -89,9 +89,7 @@ func ReadPassword(fd int) ([]byte, error) { return nil, err } - defer func() { - windows.SetConsoleMode(windows.Handle(fd), old) - }() + defer windows.SetConsoleMode(windows.Handle(fd), old) var h windows.Handle p, _ := windows.GetCurrentProcess() From 7d04dfe72db9e2ef980f2462c731417b83ab6d4b Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Thu, 8 Mar 2018 08:35:08 +0000 Subject: [PATCH 44/58] ssh/terminal: store termios copy in terminal state on Solaris TestMakeRawState currently fails on Solaris: --- FAIL: TestMakeRawState (0.00s) terminal_test.go:334: states do not match; was &{0xc420015dd0}, expected &{0xc420015da0} Change terminal.State to include a copy to the unix.Termios (like the implementation for Linux and the BSDs) which also makes terminal.MakeRaw behave as expected and lets TestMakeRawState pass. Change-Id: I29382f83b84ff301991e1db170f32f41e144aec8 Reviewed-on: https://go-review.googlesource.com/99456 Run-TryBot: Tobias Klauser TryBot-Result: Gobot Gobot Reviewed-by: Ian Lance Taylor --- util_solaris.go | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/util_solaris.go b/util_solaris.go index a2e1b57..9e41b9f 100644 --- a/util_solaris.go +++ b/util_solaris.go @@ -14,7 +14,7 @@ import ( // State contains the state of a terminal. type State struct { - state *unix.Termios + termios unix.Termios } // IsTerminal returns true if the given file descriptor is a terminal. @@ -75,47 +75,43 @@ func ReadPassword(fd int) ([]byte, error) { // restored. // see http://cr.illumos.org/~webrev/andy_js/1060/ func MakeRaw(fd int) (*State, error) { - oldTermiosPtr, err := unix.IoctlGetTermios(fd, unix.TCGETS) + termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) if err != nil { return nil, err } - oldTermios := *oldTermiosPtr - newTermios := oldTermios - newTermios.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON - newTermios.Oflag &^= syscall.OPOST - newTermios.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN - newTermios.Cflag &^= syscall.CSIZE | syscall.PARENB - newTermios.Cflag |= syscall.CS8 - newTermios.Cc[unix.VMIN] = 1 - newTermios.Cc[unix.VTIME] = 0 + oldState := State{termios: *termios} - if err := unix.IoctlSetTermios(fd, unix.TCSETS, &newTermios); err != nil { + termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + termios.Oflag &^= unix.OPOST + termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + termios.Cflag &^= unix.CSIZE | unix.PARENB + termios.Cflag |= unix.CS8 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 + + if err := unix.IoctlSetTermios(fd, unix.TCSETS, termios); err != nil { return nil, err } - return &State{ - state: oldTermiosPtr, - }, nil + return &oldState, nil } // Restore restores the terminal connected to the given file descriptor to a // previous state. func Restore(fd int, oldState *State) error { - return unix.IoctlSetTermios(fd, unix.TCSETS, oldState.state) + return unix.IoctlSetTermios(fd, unix.TCSETS, &oldState.termios) } // GetState returns the current state of a terminal which may be useful to // restore the terminal after a signal. func GetState(fd int) (*State, error) { - oldTermiosPtr, err := unix.IoctlGetTermios(fd, unix.TCGETS) + termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) if err != nil { return nil, err } - return &State{ - state: oldTermiosPtr, - }, nil + return &State{termios: *termios}, nil } // GetSize returns the dimensions of the given terminal. From c0053fbbb3a9fb8f369c02e0e049229447905c31 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 10 May 2018 04:48:04 +0800 Subject: [PATCH 45/58] ssh/terminal: run tests only on supported platforms The tag matches the platforms defined in util*.go where the most tested logic is defined. Change-Id: I90f67d988c795738c3effbc8554a933a7cb355d2 Reviewed-on: https://go-review.googlesource.com/112555 Reviewed-by: Brad Fitzpatrick --- terminal_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terminal_test.go b/terminal_test.go index 901c72a..a27cdd6 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd windows plan9 solaris + package terminal import ( From 2f5b78d13e0e44e9ccd859876ffb677d94d30192 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Thu, 24 May 2018 13:38:20 +0200 Subject: [PATCH 46/58] ssh/terminal: fix TestMakeRawState on iOS Fix the following failure on iOS: --- FAIL: TestMakeRawState (0.00s) terminal_test.go:332: failed to get terminal state from MakeRaw: operation not permitted Updates golang/go#25535 Change-Id: I1ab6feb31ba5e89dc0d5f2a1cefd56c09f178e80 Reviewed-on: https://go-review.googlesource.com/114415 Run-TryBot: Tobias Klauser TryBot-Result: Gobot Gobot Reviewed-by: Brad Fitzpatrick --- terminal_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/terminal_test.go b/terminal_test.go index a27cdd6..d9b77c1 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -10,6 +10,7 @@ import ( "bytes" "io" "os" + "runtime" "testing" ) @@ -326,6 +327,11 @@ func TestMakeRawState(t *testing.T) { if err != nil { t.Fatalf("failed to get terminal state from GetState: %s", err) } + + if runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") { + t.Skip("MakeRaw not allowed on iOS; skipping test") + } + defer Restore(fd, st) raw, err := MakeRaw(fd) if err != nil { From 9b5186374412d895b49368bef0ffdecd201ccbe9 Mon Sep 17 00:00:00 2001 From: chigotc Date: Fri, 23 Nov 2018 16:23:17 +0100 Subject: [PATCH 47/58] ssh/terminal: add AIX operating system This commit adds AIX operation system to ssh/terminal package. Change-Id: I31ccec5512dbf476eaf22ff79951b5fab434d5fd Reviewed-on: https://go-review.googlesource.com/c/151077 Run-TryBot: Tobias Klauser TryBot-Result: Gobot Gobot Reviewed-by: Tobias Klauser --- util.go | 2 +- util_aix.go | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 util_aix.go diff --git a/util.go b/util.go index 731c89a..9a3fb09 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd +// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd // Package terminal provides support functions for dealing with terminals, as // commonly found on UNIX systems. diff --git a/util_aix.go b/util_aix.go new file mode 100644 index 0000000..dfcd627 --- /dev/null +++ b/util_aix.go @@ -0,0 +1,12 @@ +// Copyright 2018 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. + +// +build aix + +package terminal + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS +const ioctlWriteTermios = unix.TCSETS From 47e6c8196c5ed1d8d0144f106f03ce0977864875 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Mon, 26 Nov 2018 10:41:34 +0100 Subject: [PATCH 48/58] ssh/terminal: use "reports whether" in IsTerminal doc Go documentation style for boolean funcs is to say: // Foo reports whether ... func Foo() bool (rather than "returns true if") Change-Id: I6972d123ba99bbf3dbf95e876b45b2ecd98dd07c Reviewed-on: https://go-review.googlesource.com/c/151257 Run-TryBot: Tobias Klauser TryBot-Result: Gobot Gobot Reviewed-by: Brad Fitzpatrick --- util.go | 2 +- util_plan9.go | 2 +- util_solaris.go | 2 +- util_windows.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/util.go b/util.go index 9a3fb09..3911040 100644 --- a/util.go +++ b/util.go @@ -25,7 +25,7 @@ type State struct { termios unix.Termios } -// IsTerminal returns true if the given file descriptor is a terminal. +// IsTerminal returns whether the given file descriptor is a terminal. func IsTerminal(fd int) bool { _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) return err == nil diff --git a/util_plan9.go b/util_plan9.go index 799f049..9317ac7 100644 --- a/util_plan9.go +++ b/util_plan9.go @@ -21,7 +21,7 @@ import ( type State struct{} -// IsTerminal returns true if the given file descriptor is a terminal. +// IsTerminal returns whether the given file descriptor is a terminal. func IsTerminal(fd int) bool { return false } diff --git a/util_solaris.go b/util_solaris.go index 9e41b9f..3d5f06a 100644 --- a/util_solaris.go +++ b/util_solaris.go @@ -17,7 +17,7 @@ type State struct { termios unix.Termios } -// IsTerminal returns true if the given file descriptor is a terminal. +// IsTerminal returns whether the given file descriptor is a terminal. func IsTerminal(fd int) bool { _, err := unix.IoctlGetTermio(fd, unix.TCGETA) return err == nil diff --git a/util_windows.go b/util_windows.go index 8618955..6cb8a95 100644 --- a/util_windows.go +++ b/util_windows.go @@ -26,7 +26,7 @@ type State struct { mode uint32 } -// IsTerminal returns true if the given file descriptor is a terminal. +// IsTerminal returns whether the given file descriptor is a terminal. func IsTerminal(fd int) bool { var st uint32 err := windows.GetConsoleMode(windows.Handle(fd), &st) From 5c4920f7cdc0bea0deadf186e24701575a6bf2da Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Tue, 27 Nov 2018 09:28:20 +0100 Subject: [PATCH 49/58] ssh/terminal: enable tests for aix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable the tests on aix after support was added in CL 151077. Change-Id: I2dcdaaa54d7c27b7697224e0f3cfab3cf0b52b6a Reviewed-on: https://go-review.googlesource.com/c/151437 Run-TryBot: Tobias Klauser TryBot-Result: Gobot Gobot Reviewed-by: Clément Chigot Reviewed-by: Ian Lance Taylor --- terminal_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal_test.go b/terminal_test.go index d9b77c1..5e5d33b 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd windows plan9 solaris +// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd windows plan9 solaris package terminal From 1fb3bd0768d0139e111842e96ac7ffe2571fe772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Bohusl=C3=A1vek?= Date: Tue, 29 Nov 2016 22:27:05 +0000 Subject: [PATCH 50/58] ssh/terminal: support ^N and ^P This makes it possible to navigate the history without leaving the home row on the keyboard. Change-Id: Id24c43f8eb6090520ab37bf8126264901b70c489 Reviewed-on: https://go-review.googlesource.com/c/33618 Run-TryBot: Matt Layher TryBot-Result: Gobot Gobot Reviewed-by: Matt Layher Reviewed-by: Brad Fitzpatrick --- terminal.go | 4 ++++ terminal_test.go | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/terminal.go b/terminal.go index 9a88759..9d666ff 100644 --- a/terminal.go +++ b/terminal.go @@ -159,6 +159,10 @@ func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { return keyClearScreen, b[1:] case 23: // ^W return keyDeleteWord, b[1:] + case 14: // ^N + return keyDown, b[1:] + case 16: // ^P + return keyUp, b[1:] } } diff --git a/terminal_test.go b/terminal_test.go index 5e5d33b..3ae9116 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -91,6 +91,12 @@ var keyPressTests = []struct { { in: "\x1b[B\r", // down }, + { + in: "\016\r", // ^P + }, + { + in: "\014\r", // ^N + }, { in: "line\x1b[A\x1b[B\r", // up then down line: "line", From 3fba1d1f88f4a9b0c3e33e9a0c1249b5849dfe66 Mon Sep 17 00:00:00 2001 From: Max Semenik Date: Sat, 23 Feb 2019 00:20:48 -0800 Subject: [PATCH 51/58] ssh/terminal: fix GetSize on Windows Return window size instead of buffer size. Fixes golang/go#27743 Change-Id: Ib1cd249f5680d86d505032e51d9102c2718ddf6f Reviewed-on: https://go-review.googlesource.com/c/163538 Reviewed-by: Brad Fitzpatrick Run-TryBot: Brad Fitzpatrick TryBot-Result: Gobot Gobot --- util_windows.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/util_windows.go b/util_windows.go index 6cb8a95..5cfdf8f 100644 --- a/util_windows.go +++ b/util_windows.go @@ -64,13 +64,15 @@ func Restore(fd int, state *State) error { return windows.SetConsoleMode(windows.Handle(fd), state.mode) } -// GetSize returns the dimensions of the given terminal. +// GetSize returns the visible dimensions of the given terminal. +// +// These dimensions don't include any scrollback buffer height. func GetSize(fd int) (width, height int, err error) { var info windows.ConsoleScreenBufferInfo if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { return 0, 0, err } - return int(info.Size.X), int(info.Size.Y), nil + return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil } // ReadPassword reads a line of input from a terminal without local echo. This From c5b4c79d1f3a679f00863bad66768e8bf4aea4bf Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 25 Mar 2019 14:57:12 +0000 Subject: [PATCH 52/58] ssh/terminal: Use move-N sequences for >1 cursor moves Before, we emitted N single-move sequences on a cursor move. For example, "move 4 left" would emit "^[[D^[[D^[[D^[[D". With this change, it would emit "^[[4D". Using variable move sequences when possible reduces the amount of rendering output that the terminal implementation produces. This can have some low-level performance benefits, but also helps consumers reason through the produced output. Includes a test with a couple of cases. Note: The old implementation used ^[[D instead of ^[D which is also valid. This is true in several unrelated places, so this implementation continues to use ^[[D for consistency. Change-Id: If38eaaed8fb4075499fdda54c06681dc34c3ad70 GitHub-Last-Rev: 92ef2538d33a9493f3df09984c277dfd8bf0abf4 GitHub-Pull-Request: golang/crypto#82 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/169077 Reviewed-by: Adam Langley Reviewed-by: Brad Fitzpatrick --- terminal.go | 63 ++++++++++++++++++++++++++++-------------------- terminal_test.go | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/terminal.go b/terminal.go index 9d666ff..2f04ee5 100644 --- a/terminal.go +++ b/terminal.go @@ -7,6 +7,7 @@ package terminal import ( "bytes" "io" + "strconv" "sync" "unicode/utf8" ) @@ -271,34 +272,44 @@ func (t *Terminal) moveCursorToPos(pos int) { } func (t *Terminal) move(up, down, left, right int) { - movement := make([]rune, 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:] + m := []rune{} + + // 1 unit up can be expressed as ^[[A or ^[A + // 5 units up can be expressed as ^[[5A + + if up == 1 { + m = append(m, keyEscape, '[', 'A') + } else if up > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(up))...) + m = append(m, 'A') } - t.queue(movement) + if down == 1 { + m = append(m, keyEscape, '[', 'B') + } else if down > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(down))...) + m = append(m, 'B') + } + + if right == 1 { + m = append(m, keyEscape, '[', 'C') + } else if right > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(right))...) + m = append(m, 'C') + } + + if left == 1 { + m = append(m, keyEscape, '[', 'D') + } else if left > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(left))...) + m = append(m, 'D') + } + + t.queue(m) } func (t *Terminal) clearLineToRight() { diff --git a/terminal_test.go b/terminal_test.go index 3ae9116..4e7a0c6 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -237,6 +237,49 @@ func TestKeyPresses(t *testing.T) { } } +var renderTests = []struct { + in string + received string + err error +}{ + { + // Cursor move after keyHome (left 4) then enter (right 4, newline) + in: "abcd\x1b[H\r", + received: "> abcd\x1b[4D\x1b[4C\r\n", + }, + { + // Write, home, prepend, enter. Prepends rewrites the line. + in: "cdef\x1b[Hab\r", + received: "> cdef" + // Initial input + "\x1b[4Da" + // Move cursor back, insert first char + "cdef" + // Copy over original string + "\x1b[4Dbcdef" + // Repeat for second char with copy + "\x1b[4D" + // Put cursor back in position to insert again + "\x1b[4C\r\n", // Put cursor at the end of the line and newline. + }, +} + +func TestRender(t *testing.T) { + for i, test := range renderTests { + for j := 1; j < len(test.in); j++ { + c := &MockTerminal{ + toSend: []byte(test.in), + bytesPerRead: j, + } + ss := NewTerminal(c, "> ") + _, err := ss.ReadLine() + if err != test.err { + t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) + break + } + if test.received != string(c.received) { + t.Errorf("Results rendered from test %d (%d bytes per read) was '%s', expected '%s'", i, j, c.received, test.received) + break + } + } + } +} + func TestPasswordNotSaved(t *testing.T) { c := &MockTerminal{ toSend: []byte("password\r\x1b[A\r"), From cf60e62b0da838f031b049c9c7052d369176fcc6 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 22 Sep 2019 23:01:56 +0200 Subject: [PATCH 53/58] ssh/terminal: account for win32 api changes The API changed for this function, since the call always succeeds. Update this user of it accordingly. Fixes: golang/go#34461 Change-Id: I9d19b70767fc6b1b9292ad8732dd8e54bb6a8ae0 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/196897 Run-TryBot: Jason A. Donenfeld TryBot-Result: Gobot Gobot Reviewed-by: Brad Fitzpatrick --- util_windows.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/util_windows.go b/util_windows.go index 5cfdf8f..61312ae 100644 --- a/util_windows.go +++ b/util_windows.go @@ -94,8 +94,7 @@ func ReadPassword(fd int) ([]byte, error) { defer windows.SetConsoleMode(windows.Handle(fd), old) var h windows.Handle - p, _ := windows.GetCurrentProcess() - if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { + if err := windows.DuplicateHandle(windows.GetCurrentProcess(), windows.Handle(fd), windows.GetCurrentProcess(), &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { return nil, err } From 24acb349b8b9ae54e6e0c93682fd7eda0e8d5683 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 26 Sep 2019 17:54:57 +0000 Subject: [PATCH 54/58] Revert "ssh/terminal: account for win32 api changes" This reverts commit CL 196897 (commit cf60e62b0da83) Reason for revert: we're reverting the API change instead in https://go-review.googlesource.com/c/sys/+/197597 Change-Id: I41741c85bb7a07b9ce13264ebb26ee3f968772fa Reviewed-on: https://go-review.googlesource.com/c/crypto/+/197598 Reviewed-by: Bryan C. Mills Run-TryBot: Bryan C. Mills --- util_windows.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util_windows.go b/util_windows.go index 61312ae..5cfdf8f 100644 --- a/util_windows.go +++ b/util_windows.go @@ -94,7 +94,8 @@ func ReadPassword(fd int) ([]byte, error) { defer windows.SetConsoleMode(windows.Handle(fd), old) var h windows.Handle - if err := windows.DuplicateHandle(windows.GetCurrentProcess(), windows.Handle(fd), windows.GetCurrentProcess(), &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { + p, _ := windows.GetCurrentProcess() + if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { return nil, err } From 31072773c25ce9b9eb7231098dcb16f0f2b6bd09 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sun, 22 Dec 2019 03:38:13 +0900 Subject: [PATCH 55/58] ssh/terminal: stop using ENABLE_LINE_INPUT ReadConsole does not read more than 254 bytes when ENABLE_LINE_INPUT is enabled. Fixes golang/go#36071 Change-Id: If5c160404b855387a80f1d57638aac3f2db1a097 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/212377 Run-TryBot: Alex Brainman TryBot-Result: Gobot Gobot Reviewed-by: Alex Brainman --- terminal.go | 4 ++++ util_windows.go | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/terminal.go b/terminal.go index 2f04ee5..dd7378c 100644 --- a/terminal.go +++ b/terminal.go @@ -947,6 +947,10 @@ func readPasswordLine(reader io.Reader) ([]byte, error) { n, err := reader.Read(buf[:]) if n > 0 { switch buf[0] { + case '\b': + if len(ret) > 0 { + ret = ret[:len(ret)-1] + } case '\n': return ret, nil case '\r': diff --git a/util_windows.go b/util_windows.go index 5cfdf8f..f614e9c 100644 --- a/util_windows.go +++ b/util_windows.go @@ -85,8 +85,8 @@ func ReadPassword(fd int) ([]byte, error) { } old := st - st &^= (windows.ENABLE_ECHO_INPUT) - st |= (windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + st &^= (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT) + st |= (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_PROCESSED_INPUT) if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil { return nil, err } From 1a268e53058a4f7946358fdfb6dc439a67c7a772 Mon Sep 17 00:00:00 2001 From: Alex Brainman Date: Sun, 19 Jan 2020 09:47:15 +1100 Subject: [PATCH 56/58] ssh/terminal: adjust ReadConsole rules on windows CL 212377 changed end of input character on windows - from \n to \r. But CL 212377 did not adjust ReadConsole accordingly. For example, after CL 212377 \n was still used to end of password processing, and \r was ignored. This CL swaps these rules - \r is now used to end password processing, and \n are ignored. The change only affects windows, all non windows code should work as before. This CL also adjusts TestReadPasswordLineEnd to fit new rules. Fixes golang/go#36609 Change-Id: I027bf80d10e7d4d4b17ff0264935d14b8bea9097 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/215417 Run-TryBot: Alex Brainman TryBot-Result: Gobot Gobot Reviewed-by: Filippo Valsorda --- terminal.go | 13 +++++++++++-- terminal_test.go | 20 +++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/terminal.go b/terminal.go index dd7378c..d1b4fca 100644 --- a/terminal.go +++ b/terminal.go @@ -7,6 +7,7 @@ package terminal import ( "bytes" "io" + "runtime" "strconv" "sync" "unicode/utf8" @@ -939,6 +940,8 @@ func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { // readPasswordLine reads from reader until it finds \n or io.EOF. // The slice returned does not include the \n. // readPasswordLine also ignores any \r it finds. +// Windows uses \r as end of line. So, on Windows, readPasswordLine +// reads until it finds \r and ignores any \n it finds during processing. func readPasswordLine(reader io.Reader) ([]byte, error) { var buf [1]byte var ret []byte @@ -952,9 +955,15 @@ func readPasswordLine(reader io.Reader) ([]byte, error) { ret = ret[:len(ret)-1] } case '\n': - return ret, nil + if runtime.GOOS != "windows" { + return ret, nil + } + // otherwise ignore \n case '\r': - // remove \r from passwords on Windows + if runtime.GOOS == "windows" { + return ret, nil + } + // otherwise ignore \r default: ret = append(ret, buf[0]) } diff --git a/terminal_test.go b/terminal_test.go index 4e7a0c6..2a2facc 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -323,18 +323,32 @@ func TestTerminalSetSize(t *testing.T) { } func TestReadPasswordLineEnd(t *testing.T) { - var tests = []struct { + type testType struct { input string want string - }{ - {"\n", ""}, + } + var tests = []testType{ {"\r\n", ""}, {"test\r\n", "test"}, + {"test\r", "test"}, + {"test\n", "test"}, {"testtesttesttes\n", "testtesttesttes"}, {"testtesttesttes\r\n", "testtesttesttes"}, {"testtesttesttesttest\n", "testtesttesttesttest"}, {"testtesttesttesttest\r\n", "testtesttesttesttest"}, + {"\btest", "test"}, + {"t\best", "est"}, + {"te\bst", "tst"}, + {"test\b", "tes"}, + {"test\b\r\n", "tes"}, + {"test\b\n", "tes"}, + {"test\b\r", "tes"}, } + eol := "\n" + if runtime.GOOS == "windows" { + eol = "\r" + } + tests = append(tests, testType{eol, ""}) for _, test := range tests { buf := new(bytes.Buffer) if _, err := buf.WriteString(test.input); err != nil { From 07bee379ff232d28fe3d7721656636dd3e0d3cfc Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 14 Apr 2020 11:39:45 -0400 Subject: [PATCH 57/58] ssh/terminal: handle ctrl+C, ctrl+F, ctrl+B MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ctrl+C: terminate readline, which may result in application termination. ctrl+F: keyRight ctrl+B: keyLeft Update golang/go#27147 Change-Id: If319ef79708b98c030cbce102400a785d15137f8 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/228223 Reviewed-by: Daniel Martí Run-TryBot: Daniel Martí TryBot-Result: Gobot Gobot --- terminal.go | 8 ++++++++ terminal_test.go | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/terminal.go b/terminal.go index d1b4fca..2ffb97b 100644 --- a/terminal.go +++ b/terminal.go @@ -113,6 +113,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal { } const ( + keyCtrlC = 3 keyCtrlD = 4 keyCtrlU = 21 keyEnter = '\r' @@ -151,8 +152,12 @@ func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { switch b[0] { case 1: // ^A return keyHome, b[1:] + case 2: // ^B + return keyLeft, b[1:] case 5: // ^E return keyEnd, b[1:] + case 6: // ^F + return keyRight, b[1:] case 8: // ^H return keyBackspace, b[1:] case 11: // ^K @@ -738,6 +743,9 @@ func (t *Terminal) readLine() (line string, err error) { return "", io.EOF } } + if key == keyCtrlC { + return "", io.EOF + } if key == keyPasteStart { t.pasteActive = true if len(t.line) == 0 { diff --git a/terminal_test.go b/terminal_test.go index 2a2facc..c99638d 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -81,6 +81,14 @@ var keyPressTests = []struct { in: "a\x1b[Db\r", // left line: "ba", }, + { + in: "a\006b\r", // ^F + line: "ab", + }, + { + in: "a\002b\r", // ^B + line: "ba", + }, { in: "a\177b\r", // backspace line: "b", @@ -208,6 +216,16 @@ var keyPressTests = []struct { line: "a", err: ErrPasteIndicator, }, + { + // Ctrl-C terminates readline + in: "\003", + err: io.EOF, + }, + { + // Ctrl-C at the end of line also terminates readline + in: "a\003\r", + err: io.EOF, + }, } func TestKeyPresses(t *testing.T) { From c955d0b553c8bdbd6b706f42bc6fea84ea58a6e1 Mon Sep 17 00:00:00 2001 From: Mahdi Hosseini Moghaddam Date: Tue, 10 Nov 2020 19:44:30 -0800 Subject: [PATCH 58/58] ssh/terminal: add support for zos Fixes golang/go#42496 Change-Id: Iae2ddb916904d9b3947bec9638c9fbf892df7b7c Reviewed-on: https://go-review.googlesource.com/c/crypto/+/269177 Reviewed-by: Tobias Klauser Trust: Tobias Klauser Trust: Michael Munday Run-TryBot: Tobias Klauser TryBot-Result: Go Bot --- util.go | 2 +- util_zos.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 util_zos.go diff --git a/util.go b/util.go index 3911040..73b370a 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd +// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd zos // Package terminal provides support functions for dealing with terminals, as // commonly found on UNIX systems. diff --git a/util_zos.go b/util_zos.go new file mode 100644 index 0000000..8314a2d --- /dev/null +++ b/util_zos.go @@ -0,0 +1,10 @@ +// Copyright 2020 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 terminal + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS +const ioctlWriteTermios = unix.TCSETS