From 0f1b7666bb44186fc77ae22b43f44bda43bde77f Mon Sep 17 00:00:00 2001 From: Sunny Date: Sun, 12 Apr 2020 01:04:54 +0530 Subject: [PATCH 1/6] Unify ssh and exec This change combines ssh and exec to use the same code. ssh no longer uses ssh binary on the host but uses go's crypto/ssh package to create a terminal session. Moves newSignerForKey, newSSHConfig and joinShellCommand functions from exec.go to ssh.go, keeping all ssh related functions in the same file. --- cmd/ignite/run/exec.go | 116 +-- cmd/ignite/run/ssh.go | 173 +++- .../x/crypto/ssh/terminal/terminal.go | 966 ++++++++++++++++++ .../golang.org/x/crypto/ssh/terminal/util.go | 114 +++ .../x/crypto/ssh/terminal/util_aix.go | 12 + .../x/crypto/ssh/terminal/util_bsd.go | 12 + .../x/crypto/ssh/terminal/util_linux.go | 10 + .../x/crypto/ssh/terminal/util_plan9.go | 58 ++ .../x/crypto/ssh/terminal/util_solaris.go | 124 +++ .../x/crypto/ssh/terminal/util_windows.go | 105 ++ 10 files changed, 1540 insertions(+), 150 deletions(-) create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/terminal.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_aix.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_linux.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_windows.go diff --git a/cmd/ignite/run/exec.go b/cmd/ignite/run/exec.go index cb2eb3193..c4751b24e 100644 --- a/cmd/ignite/run/exec.go +++ b/cmd/ignite/run/exec.go @@ -1,18 +1,7 @@ package run import ( - "fmt" - "io/ioutil" - "net" - "os" - "path" - "time" - - "github.com/alessio/shellescape" api "github.com/weaveworks/ignite/pkg/apis/ignite" - "github.com/weaveworks/ignite/pkg/constants" - "github.com/weaveworks/ignite/pkg/util" - "golang.org/x/crypto/ssh" ) // ExecFlags contains the flags supported by the exec command. @@ -41,108 +30,5 @@ func (ef *ExecFlags) NewExecOptions(vmMatch string, command ...string) (eo *exec // Exec executes command in a VM based on the provided execOptions. func Exec(eo *execOptions) error { - // Check if the VM is running - if !eo.vm.Running() { - return fmt.Errorf("VM %q is not running", eo.vm.GetUID()) - } - - // Get the IP address - ipAddrs := eo.vm.Status.IPAddresses - if len(ipAddrs) == 0 { - return fmt.Errorf("VM %q has no usable IP addresses", eo.vm.GetUID()) - } - - // If an external identity file is specified, use it instead of the internal one - privKeyFile := eo.IdentityFile - if len(privKeyFile) == 0 { - privKeyFile = path.Join(eo.vm.ObjectPath(), fmt.Sprintf(constants.VM_SSH_KEY_TEMPLATE, eo.vm.GetUID())) - if !util.FileExists(privKeyFile) { - return fmt.Errorf("no private key found for VM %q", eo.vm.GetUID()) - } - } - - signer, err := newSignerForKey(privKeyFile) - if err != nil { - return fmt.Errorf("unable to create signer for private key: %v", err) - } - - // Create an SSH client, and connect, we will use this to exec - config := newSSHConfig(signer, eo.Timeout) - client, err := ssh.Dial("tcp", net.JoinHostPort(ipAddrs[0].String(), "22"), config) - if err != nil { - return fmt.Errorf("failed to dial: %v", err) - } - - // Run the command, DO NOT wrap this error as the caller can check for the command exit - // code in the ssh.ExitError type - return runSSHCommand(client, eo.Tty, eo.command) -} - -func newSignerForKey(keyPath string) (ssh.Signer, error) { - key, err := ioutil.ReadFile(keyPath) - if err != nil { - return nil, fmt.Errorf("unable to read private key: %v", err) - } - - // Create the Signer for this private key. - return ssh.ParsePrivateKey(key) -} - -func newSSHConfig(publicKey ssh.Signer, timeout uint32) *ssh.ClientConfig { - return &ssh.ClientConfig{ - User: "root", - Auth: []ssh.AuthMethod{ - ssh.PublicKeys(publicKey), - }, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: use ssh.FixedPublicKey instead - Timeout: time.Second * time.Duration(timeout), - } -} - -func runSSHCommand(client *ssh.Client, tty bool, command []string) error { - // create a session for the command - session, err := client.NewSession() - if err != nil { - return fmt.Errorf("failed to create session: %v", err) - } - defer session.Close() - - if tty { - // get a pty - // TODO: should these be based on the host terminal? - // TODO: should we request something other than xterm? - // TODO: we should probably configure the terminal modes - modes := ssh.TerminalModes{} - if err := session.RequestPty("xterm", 80, 40, modes); err != nil { - return fmt.Errorf("request for pseudo terminal failed: %v", err) - } - } - - // Connect input / output - // TODO: these should come from the cobra command instead of hardcoding os.Stderr etc. - session.Stderr = os.Stderr - session.Stdout = os.Stdout - session.Stdin = os.Stdin - - /* - Do not wrap this error so the caller can check for the exit code - If the remote server does not send an exit status, an error of type *ExitMissingError is returned. - If the command completes unsuccessfully or is interrupted by a signal, the error is of type *ExitError. - Other error types may be returned for I/O problems. - */ - return session.Run(joinShellCommand(command)) -} - -// joinShellCommand joins command parts into a single string safe for passing to sh -c (or SSH) -func joinShellCommand(command []string) string { - joined := command[0] - if len(command) == 1 { - return joined - } - for _, arg := range command[1:] { - // NOTE: we need to escape / quote to ensure that - // each component of command... is read as a single shell word - joined += " " + shellescape.Quote(arg) - } - return joined + return runSSH(eo.vm, eo.IdentityFile, eo.command, eo.Tty, eo.Timeout) } diff --git a/cmd/ignite/run/ssh.go b/cmd/ignite/run/ssh.go index cf530351f..196a99b6f 100644 --- a/cmd/ignite/run/ssh.go +++ b/cmd/ignite/run/ssh.go @@ -2,14 +2,28 @@ package run import ( "fmt" + "io/ioutil" + "net" + "os" "path" + "time" + + "github.com/alessio/shellescape" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/terminal" - log "github.com/sirupsen/logrus" api "github.com/weaveworks/ignite/pkg/apis/ignite" "github.com/weaveworks/ignite/pkg/constants" "github.com/weaveworks/ignite/pkg/util" ) +const ( + defaultTerm = "xterm" + defaultSSHPort = "22" + defaultSSHNetwork = "tcp" +) + +// SSHFlags contains the flags supported by the ssh command. type SSHFlags struct { Timeout uint32 IdentityFile string @@ -20,64 +34,153 @@ type sshOptions struct { vm *api.VM } +// NewSSHOptions returns ssh options for a given VM. func (sf *SSHFlags) NewSSHOptions(vmMatch string) (so *sshOptions, err error) { so = &sshOptions{SSHFlags: sf} so.vm, err = getVMForMatch(vmMatch) return } +// SSH starts a ssh session as per the provided ssh options. func SSH(so *sshOptions) error { - // Check if the VM is running - if !so.vm.Running() { - return fmt.Errorf("VM %q is not running", so.vm.GetUID()) + return runSSH(so.vm, so.IdentityFile, []string{}, true, so.Timeout) +} + +// runSSH creates and runs ssh session based on the provided arguments. +// If the command list is empty, ssh shell is created, else the ssh command is +// executed. +func runSSH(vm *api.VM, privKeyFile string, command []string, tty bool, timeout uint32) error { + // Check if the VM is running. + if !vm.Running() { + return fmt.Errorf("VM %q is not running", vm.GetUID()) } - ipAddrs := so.vm.Status.IPAddresses + // Get the IP address. + ipAddrs := vm.Status.IPAddresses if len(ipAddrs) == 0 { - return fmt.Errorf("VM %q has no usable IP addresses", so.vm.GetUID()) + return fmt.Errorf("VM %q has no usable IP addresses", vm.GetUID()) + } + + // Get private key file path. + if len(privKeyFile) == 0 { + privKeyFile = path.Join(vm.ObjectPath(), fmt.Sprintf(constants.VM_SSH_KEY_TEMPLATE, vm.GetUID())) + if !util.FileExists(privKeyFile) { + return fmt.Errorf("no private key found for VM %q", vm.GetUID()) + } } - // We're dealing with local VMs in a trusted (internal) subnet, disable some warnings for convenience - // TODO: For security, track the known_hosts internally, do something about the IP collisions (if needed) - sshOpts := []string{ - "LogLevel=ERROR", // Warning: Permanently added '' (ECDSA) to the list of known hosts. - // We get this if the VM happens to get an address that another container has used: - "UserKnownHostsFile=/dev/null", // WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! - "StrictHostKeyChecking=no", // The authenticity of host ***** can't be established - fmt.Sprintf("ConnectTimeout=%d", so.Timeout), + // Create a new ssh signer for the private key. + signer, err := newSignerForKey(privKeyFile) + if err != nil { + return fmt.Errorf("unable to create signer for private key: %v", err) } - sshArgs := append(make([]string, 0, len(sshOpts)*2+3), - fmt.Sprintf("root@%s", ipAddrs[0])) + // Create an SSH client, and connect. + config := newSSHConfig(signer, timeout) + client, err := ssh.Dial(defaultSSHNetwork, net.JoinHostPort(ipAddrs[0].String(), defaultSSHPort), config) + if err != nil { + return fmt.Errorf("failed to dial: %v", err) + } + defer client.Close() - for _, opt := range sshOpts { - sshArgs = append(sshArgs, "-o", opt) + // Create a session. + session, err := client.NewSession() + if err != nil { + return fmt.Errorf("failed to create session: %v", err) } + defer session.Close() - sshArgs = append(sshArgs, "-i") + // Configure tty if requested. + if tty { + // Get stdin file descriptor reference. + fd := int(os.Stdin.Fd()) - // If an external identity file is specified, use it instead of the internal one - if len(so.IdentityFile) > 0 { - sshArgs = append(sshArgs, so.IdentityFile) - } else { - privKeyFile := path.Join(so.vm.ObjectPath(), fmt.Sprintf(constants.VM_SSH_KEY_TEMPLATE, so.vm.GetUID())) - if !util.FileExists(privKeyFile) { - return fmt.Errorf("no private key found for VM %q", so.vm.GetUID()) + // Store the raw state of the terminal. + state, err := terminal.MakeRaw(fd) + if err != nil { + return fmt.Errorf("failed to make terminal raw: %v", err) } + defer terminal.Restore(fd, state) - sshArgs = append(sshArgs, privKeyFile) + // Get the terminal dimensions. + w, h, err := terminal.GetSize(fd) + if err != nil { + return fmt.Errorf("failed to get terminal size: %v", err) + } + + // Set terminal modes. + modes := ssh.TerminalModes{ + ssh.ECHO: 1, + } + if err := session.RequestPty(defaultTerm, h, w, modes); err != nil { + return fmt.Errorf("request for pseudo terminal failed: %v", err) + } } - // SSH into the VM - if code, err := util.ExecForeground("ssh", sshArgs...); err != nil { - if code != 255 { - return fmt.Errorf("SSH into VM %q failed: %v", so.vm.GetUID(), err) + // Connect input / output. + // TODO: these should come from the cobra command instead of hardcoding + // os.Stderr etc. + session.Stderr = os.Stderr + session.Stdout = os.Stdout + session.Stdin = os.Stdin + + if len(command) == 0 { + if err := session.Shell(); err != nil { + return fmt.Errorf("failed to start shell: %v", err) } - // Code 255 is used for signaling a connection error, be it caused by - // a failed connection attempt or disconnection by VM reboot. - log.Warnf("SSH command terminated") + if err := session.Wait(); err != nil { + if e, ok := err.(*ssh.ExitError); ok { + switch e.ExitStatus() { + case 130: + // When Ctrl-C is pressed during the ssh session, exit ends + // with error: + // failed waiting for session to exit: Process exited with status 130 + // Ignore this error for clean exit. + return nil + } + } + return fmt.Errorf("failed waiting for session to exit: %v", err) + } + } else { + if err := session.Run(joinShellCommand(command)); err != nil { + return fmt.Errorf("failed to run shell command: %s", err) + } } - return nil } + +func newSignerForKey(keyPath string) (ssh.Signer, error) { + key, err := ioutil.ReadFile(keyPath) + if err != nil { + return nil, fmt.Errorf("unable to read private key: %v", err) + } + + // Create the Signer for this private key. + return ssh.ParsePrivateKey(key) +} + +func newSSHConfig(publicKey ssh.Signer, timeout uint32) *ssh.ClientConfig { + return &ssh.ClientConfig{ + User: "root", + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(publicKey), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: use ssh.FixedPublicKey instead + Timeout: time.Second * time.Duration(timeout), + } +} + +// joinShellCommand joins command parts into a single string safe for passing to sh -c (or SSH) +func joinShellCommand(command []string) string { + joined := command[0] + if len(command) == 1 { + return joined + } + for _, arg := range command[1:] { + // NOTE: we need to escape / quote to ensure that + // each component of command... is read as a single shell word + joined += " " + shellescape.Quote(arg) + } + return joined +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/terminal.go b/vendor/golang.org/x/crypto/ssh/terminal/terminal.go new file mode 100644 index 000000000..2f04ee5b5 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/terminal.go @@ -0,0 +1,966 @@ +// 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 ( + "bytes" + "io" + "strconv" + "sync" + "unicode/utf8" +) + +// 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 (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 + // 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 []rune + + // line is the current line being entered. + line []rune + // pos is the logical position of the cursor in line + 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 + // 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 + + // 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 +// 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: []rune(prompt), + termWidth: 80, + termHeight: 24, + echo: true, + historyIndex: -1, + } +} + +const ( + keyCtrlD = 4 + keyCtrlU = 21 + keyEnter = '\r' + keyEscape = 27 + keyBackspace = 127 + keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota + keyUp + keyDown + keyLeft + keyRight + keyAltLeft + keyAltRight + keyHome + keyEnd + keyDeleteWord + keyDeleteLine + keyClearScreen + keyPasteStart + keyPasteEnd +) + +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. +func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { + if len(b) == 0 { + return utf8.RuneError, nil + } + + 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:] + case 14: // ^N + return keyDown, b[1:] + case 16: // ^P + return keyUp, b[1:] + } + } + + if b[0] != keyEscape { + if !utf8.FullRune(b) { + return utf8.RuneError, b + } + r, l := utf8.DecodeRune(b) + return r, b[l:] + } + + if !pasteActive && 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:] + case 'H': + return keyHome, b[3:] + case 'F': + return keyEnd, b[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:] + case 'D': + return keyAltLeft, b[6:] + } + } + + 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 + // appears at the end of a sequence. + for i, c := range b[0:] { + if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' { + return keyUnknown, b[i+1:] + } + } + + return utf8.RuneError, b +} + +// queue appends data to the end of t.outBuf +func (t *Terminal) queue(data []rune) { + t.outBuf = append(t.outBuf, []byte(string(data))...) +} + +var eraseUnderCursor = []rune{' ', keyEscape, '[', 'D'} +var space = []rune{' '} + +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 +// given, logical position in the text. +func (t *Terminal) moveCursorToPos(pos int) { + if !t.echo { + return + } + + x := visualLength(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) { + 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') + } + + 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() { + op := []rune{keyEscape, '[', 'K'} + t.queue(op) +} + +const maxLineLength = 4096 + +func (t *Terminal) setLine(newLine []rune, 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 +} + +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, '\r', '\n') + } +} + +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.advanceCursor(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 +} + +// 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) { + if t.pasteActive && key != keyEnter { + t.addKeyToLine(key) + return + } + + switch key { + case keyBackspace: + if t.pos == 0 { + return + } + t.eraseNPreviousChars(1) + case keyAltLeft: + // move left by a word. + t.pos -= t.countToLeftWord() + t.moveCursorToPos(t.pos) + case keyAltRight: + // move right by a word. + t.pos += t.countToRightWord() + 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 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++ + runes := []rune(entry) + t.setLine(runes, len(runes)) + case keyDown: + switch t.historyIndex { + case -1: + return + case 0: + runes := []rune(t.historyPending) + t.setLine(runes, len(runes)) + t.historyIndex-- + default: + entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) + if ok { + t.historyIndex-- + runes := []rune(entry) + t.setLine(runes, len(runes)) + } + } + case keyEnter: + t.moveCursorToPos(len(t.line)) + t.queue([]rune("\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 + 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.advanceCursor(1) + } + 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(t.prompt) + t.cursorX, t.cursorY = 0, 0 + t.advanceCursor(visualLength(t.prompt)) + t.setLine(t.line, t.pos) + default: + if t.AutoCompleteCallback != nil { + prefix := string(t.line[:t.pos]) + suffix := string(t.line[t.pos:]) + + t.lock.Unlock() + newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) + t.lock.Lock() + + if completeOk { + t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) + return + } + } + if !isPrintable(key) { + return + } + if len(t.line) == maxLineLength { + return + } + 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 + todo := len(line) + if todo > remainingOnLine { + todo = remainingOnLine + } + t.queue(line[:todo]) + t.advanceCursor(visualLength(line[:todo])) + line = line[todo:] + } +} + +// 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') + 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++ + buf = buf[1:] + } + } + + return n, nil +} + +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 writeWithCRLF(t.c, 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 = writeWithCRLF(t.c, buf); err != nil { + return + } + + t.writeLine(t.prompt) + if t.echo { + t.writeLine(t.line) + } + + 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 = []rune(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(t.prompt) + t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + } + + lineIsPasted := t.pasteActive + + for { + rest := t.remainder + lineOk := false + for !lineOk { + var key rune + key, rest = bytesToKey(rest, t.pasteActive) + if key == utf8.RuneError { + break + } + 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) + } + 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 { + if t.echo { + t.historyIndex = -1 + t.history.Add(line) + } + if lineIsPasted { + err = ErrPasteIndicator + } + 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)] + } +} + +// 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 = []rune(prompt) +} + +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() + + if width == 0 { + width = 1 + } + + oldWidth := t.termWidth + t.termWidth, t.termHeight = width, height + + switch { + case width == oldWidth: + // 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 + // 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. + if t.cursorX >= t.termWidth { + t.cursorX = t.termWidth - 1 + } + 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 +} + +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. + 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 +} + +// 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 n > 0 { + switch buf[0] { + case '\n': + return ret, nil + case '\r': + // remove \r from passwords on Windows + default: + ret = append(ret, buf[0]) + } + continue + } + if err != nil { + if err == io.EOF && len(ret) > 0 { + return ret, nil + } + return ret, err + } + } +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util.go b/vendor/golang.org/x/crypto/ssh/terminal/util.go new file mode 100644 index 000000000..391104084 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util.go @@ -0,0 +1,114 @@ +// 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 aix darwin dragonfly freebsd linux,!appengine netbsd openbsd + +// 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 "golang.org/x/crypto/ssh/terminal" + +import ( + "golang.org/x/sys/unix" +) + +// State contains the state of a terminal. +type State struct { + termios unix.Termios +} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + _, 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) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + oldState := State{termios: *termios} + + // This attempts to replicate the behaviour documented for cfmakeraw in + // the termios(3) manpage. + 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 + } + + 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) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + 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 { + return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) +} + +// 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 -1, -1, err + } + 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 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) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + 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 unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) + + return readPasswordLine(passwordReader(fd)) +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_aix.go b/vendor/golang.org/x/crypto/ssh/terminal/util_aix.go new file mode 100644 index 000000000..dfcd62785 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/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 diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go b/vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go new file mode 100644 index 000000000..cb23a5904 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/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 dragonfly freebsd netbsd openbsd + +package terminal + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TIOCGETA +const ioctlWriteTermios = unix.TIOCSETA diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_linux.go b/vendor/golang.org/x/crypto/ssh/terminal/util_linux.go new file mode 100644 index 000000000..5fadfe8a1 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_linux.go @@ -0,0 +1,10 @@ +// 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. + +package terminal + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS +const ioctlWriteTermios = unix.TCSETS diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go b/vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go new file mode 100644 index 000000000..9317ac7ed --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/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 whether 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) +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go b/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go new file mode 100644 index 000000000..3d5f06a9f --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go @@ -0,0 +1,124 @@ +// 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 unix.Termios +} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + _, err := unix.IoctlGetTermio(fd, unix.TCGETA) + 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 +} + +// 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) { + termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) + if err != nil { + return nil, err + } + + oldState := State{termios: *termios} + + 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 &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.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) { + termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) + if err != nil { + return nil, err + } + + return &State{termios: *termios}, 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 +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go b/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go new file mode 100644 index 000000000..5cfdf8f3f --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go @@ -0,0 +1,105 @@ +// 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 ( + "os" + + "golang.org/x/sys/windows" +) + +type State struct { + mode uint32 +} + +// 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) + 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 st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + 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 +} + +// 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 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + 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 { + return windows.SetConsoleMode(windows.Handle(fd), state.mode) +} + +// 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.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 +// 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 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + old := st + + 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 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 { + return nil, err + } + + f := os.NewFile(uintptr(h), "stdin") + defer f.Close() + return readPasswordLine(f) +} From 78c0fcc97804b9ee029570e5097ea0551be9b1a7 Mon Sep 17 00:00:00 2001 From: Sunny Date: Sat, 18 Apr 2020 23:06:35 +0530 Subject: [PATCH 2/6] ssh: Read TERM from the environment for PTY request --- cmd/ignite/run/ssh.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/ignite/run/ssh.go b/cmd/ignite/run/ssh.go index 196a99b6f..b22fff7bd 100644 --- a/cmd/ignite/run/ssh.go +++ b/cmd/ignite/run/ssh.go @@ -112,7 +112,14 @@ func runSSH(vm *api.VM, privKeyFile string, command []string, tty bool, timeout modes := ssh.TerminalModes{ ssh.ECHO: 1, } - if err := session.RequestPty(defaultTerm, h, w, modes); err != nil { + + // Read the TERM environment variable and use it to request the PTY. + term := os.Getenv("TERM") + if term == "" { + term = defaultTerm + } + + if err := session.RequestPty(term, h, w, modes); err != nil { return fmt.Errorf("request for pseudo terminal failed: %v", err) } } From 63b0edb88c5ad3ec9b8acfe9adaeb1cc00e11e77 Mon Sep 17 00:00:00 2001 From: Sunny Date: Sat, 18 Apr 2020 23:18:08 +0530 Subject: [PATCH 3/6] ssh: Add flag tty for configuring TTY Breaking change. Short flag -t changed from timeout to tty, similar to the exec command. --- cmd/ignite/cmd/vmcmd/ssh.go | 3 ++- cmd/ignite/run/ssh.go | 3 ++- docs/cli/ignite/ignite_ssh.md | 3 ++- docs/cli/ignite/ignite_vm_ssh.md | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/ignite/cmd/vmcmd/ssh.go b/cmd/ignite/cmd/vmcmd/ssh.go index 193479897..9c1b16d33 100644 --- a/cmd/ignite/cmd/vmcmd/ssh.go +++ b/cmd/ignite/cmd/vmcmd/ssh.go @@ -42,5 +42,6 @@ func NewCmdSSH(out io.Writer) *cobra.Command { func addSSHFlags(fs *pflag.FlagSet, sf *run.SSHFlags) { fs.StringVarP(&sf.IdentityFile, "identity", "i", "", "Override the vm's default identity file") - fs.Uint32VarP(&sf.Timeout, "timeout", "t", 10, "Timeout waiting for connection in seconds") + fs.Uint32Var(&sf.Timeout, "timeout", 10, "Timeout waiting for connection in seconds") + fs.BoolVarP(&sf.Tty, "tty", "t", true, "Allocate a pseudo-TTY") } diff --git a/cmd/ignite/run/ssh.go b/cmd/ignite/run/ssh.go index b22fff7bd..1a4d4f51d 100644 --- a/cmd/ignite/run/ssh.go +++ b/cmd/ignite/run/ssh.go @@ -27,6 +27,7 @@ const ( type SSHFlags struct { Timeout uint32 IdentityFile string + Tty bool } type sshOptions struct { @@ -43,7 +44,7 @@ func (sf *SSHFlags) NewSSHOptions(vmMatch string) (so *sshOptions, err error) { // SSH starts a ssh session as per the provided ssh options. func SSH(so *sshOptions) error { - return runSSH(so.vm, so.IdentityFile, []string{}, true, so.Timeout) + return runSSH(so.vm, so.IdentityFile, []string{}, so.Tty, so.Timeout) } // runSSH creates and runs ssh session based on the provided arguments. diff --git a/docs/cli/ignite/ignite_ssh.md b/docs/cli/ignite/ignite_ssh.md index dd3893c97..81461a3b9 100644 --- a/docs/cli/ignite/ignite_ssh.md +++ b/docs/cli/ignite/ignite_ssh.md @@ -20,7 +20,8 @@ ignite ssh [flags] ``` -h, --help help for ssh -i, --identity string Override the vm's default identity file - -t, --timeout uint32 Timeout waiting for connection in seconds (default 10) + --timeout uint32 Timeout waiting for connection in seconds (default 10) + -t, --tty Allocate a pseudo-TTY (default true) ``` ### Options inherited from parent commands diff --git a/docs/cli/ignite/ignite_vm_ssh.md b/docs/cli/ignite/ignite_vm_ssh.md index da04d4189..22a756f5a 100644 --- a/docs/cli/ignite/ignite_vm_ssh.md +++ b/docs/cli/ignite/ignite_vm_ssh.md @@ -20,7 +20,8 @@ ignite vm ssh [flags] ``` -h, --help help for ssh -i, --identity string Override the vm's default identity file - -t, --timeout uint32 Timeout waiting for connection in seconds (default 10) + --timeout uint32 Timeout waiting for connection in seconds (default 10) + -t, --tty Allocate a pseudo-TTY (default true) ``` ### Options inherited from parent commands From 5b7c7b89ee2dfad2af5279f4603ab69f682c9026 Mon Sep 17 00:00:00 2001 From: Sunny Date: Sun, 19 Apr 2020 00:24:49 +0530 Subject: [PATCH 4/6] ssh: Return actual ssh exit code When the ssh session ends, the actual ssh exit code should be returned. This change adds a deferred os.Exit and sets any ssh error code received as the exit code. --- cmd/ignite/run/ssh.go | 49 +++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/cmd/ignite/run/ssh.go b/cmd/ignite/run/ssh.go index 1a4d4f51d..8cea13e10 100644 --- a/cmd/ignite/run/ssh.go +++ b/cmd/ignite/run/ssh.go @@ -76,18 +76,39 @@ func runSSH(vm *api.VM, privKeyFile string, command []string, tty bool, timeout return fmt.Errorf("unable to create signer for private key: %v", err) } + // Defer exit here and set the exit code based on any ssh error, so that + // this ssh command returns the correct ssh exit code. Since this function + // results in an os.Exit, any error returned by this function won't be + // received by the caller. Print the error to make the errror message + // visible and set the error code when an error is found. + exitCode := 0 + defer func() { + os.Exit(exitCode) + }() + + // printErrAndSetExitCode is used to print an error message, set exit code + // and return nil. This is needed because once the ssh connection is + // estabilish, to return the error code of the actual ssh session, instead + // of returning an error, the runSSH function defers os.Exit with the ssh + // exit code. For showing any error to the user, it needs to be printed. + printErrAndSetExitCode := func(errMsg error, exitCode *int, code int) error { + fmt.Printf("%v\n", errMsg) + *exitCode = code + return nil + } + // Create an SSH client, and connect. config := newSSHConfig(signer, timeout) client, err := ssh.Dial(defaultSSHNetwork, net.JoinHostPort(ipAddrs[0].String(), defaultSSHPort), config) if err != nil { - return fmt.Errorf("failed to dial: %v", err) + return printErrAndSetExitCode(fmt.Errorf("failed to dial: %v", err), &exitCode, 1) } defer client.Close() // Create a session. session, err := client.NewSession() if err != nil { - return fmt.Errorf("failed to create session: %v", err) + return printErrAndSetExitCode(fmt.Errorf("failed to create session: %v", err), &exitCode, 1) } defer session.Close() @@ -99,14 +120,14 @@ func runSSH(vm *api.VM, privKeyFile string, command []string, tty bool, timeout // Store the raw state of the terminal. state, err := terminal.MakeRaw(fd) if err != nil { - return fmt.Errorf("failed to make terminal raw: %v", err) + return printErrAndSetExitCode(fmt.Errorf("failed to make terminal raw: %v", err), &exitCode, 1) } defer terminal.Restore(fd, state) // Get the terminal dimensions. w, h, err := terminal.GetSize(fd) if err != nil { - return fmt.Errorf("failed to get terminal size: %v", err) + return printErrAndSetExitCode(fmt.Errorf("failed to get terminal size: %v", err), &exitCode, 1) } // Set terminal modes. @@ -121,7 +142,7 @@ func runSSH(vm *api.VM, privKeyFile string, command []string, tty bool, timeout } if err := session.RequestPty(term, h, w, modes); err != nil { - return fmt.Errorf("request for pseudo terminal failed: %v", err) + return printErrAndSetExitCode(fmt.Errorf("request for pseudo terminal failed: %v", err), &exitCode, 1) } } @@ -134,25 +155,21 @@ func runSSH(vm *api.VM, privKeyFile string, command []string, tty bool, timeout if len(command) == 0 { if err := session.Shell(); err != nil { - return fmt.Errorf("failed to start shell: %v", err) + return printErrAndSetExitCode(fmt.Errorf("failed to start shell: %v", err), &exitCode, 1) } if err := session.Wait(); err != nil { if e, ok := err.(*ssh.ExitError); ok { - switch e.ExitStatus() { - case 130: - // When Ctrl-C is pressed during the ssh session, exit ends - // with error: - // failed waiting for session to exit: Process exited with status 130 - // Ignore this error for clean exit. - return nil - } + return printErrAndSetExitCode(err, &exitCode, e.ExitStatus()) } - return fmt.Errorf("failed waiting for session to exit: %v", err) + return printErrAndSetExitCode(fmt.Errorf("failed waiting for session to exit: %v", err), &exitCode, 1) } } else { if err := session.Run(joinShellCommand(command)); err != nil { - return fmt.Errorf("failed to run shell command: %s", err) + if e, ok := err.(*ssh.ExitError); ok { + return printErrAndSetExitCode(err, &exitCode, e.ExitStatus()) + } + return printErrAndSetExitCode(fmt.Errorf("failed to run shell command: %s", err), &exitCode, 1) } } return nil From d172456f72724b7dbfce25cb6a676800f12ab002 Mon Sep 17 00:00:00 2001 From: Sunny Date: Sun, 19 Apr 2020 00:43:42 +0530 Subject: [PATCH 5/6] make tidy --- .../x/crypto/ssh/terminal/terminal.go | 17 +++++++++++++++-- .../x/crypto/ssh/terminal/util_windows.go | 4 ++-- vendor/modules.txt | 1 + 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/vendor/golang.org/x/crypto/ssh/terminal/terminal.go b/vendor/golang.org/x/crypto/ssh/terminal/terminal.go index 2f04ee5b5..d1b4fca3a 100644 --- a/vendor/golang.org/x/crypto/ssh/terminal/terminal.go +++ b/vendor/golang.org/x/crypto/ssh/terminal/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 @@ -947,10 +950,20 @@ 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 + 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/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go b/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go index 5cfdf8f3f..f614e9cb6 100644 --- a/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go +++ b/vendor/golang.org/x/crypto/ssh/terminal/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 } diff --git a/vendor/modules.txt b/vendor/modules.txt index 91688db8b..9614afc92 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -401,6 +401,7 @@ golang.org/x/crypto/internal/subtle golang.org/x/crypto/poly1305 golang.org/x/crypto/ssh golang.org/x/crypto/ssh/internal/bcrypt_pbkdf +golang.org/x/crypto/ssh/terminal # golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 golang.org/x/net/bpf golang.org/x/net/context From d92a74b4076455d82dd5f70945c581e3203575b2 Mon Sep 17 00:00:00 2001 From: Sunny Date: Mon, 20 Apr 2020 21:03:50 +0530 Subject: [PATCH 6/6] ssh: log error message message to stderr --- cmd/ignite/run/ssh.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/ignite/run/ssh.go b/cmd/ignite/run/ssh.go index 8cea13e10..c31d8f437 100644 --- a/cmd/ignite/run/ssh.go +++ b/cmd/ignite/run/ssh.go @@ -9,6 +9,7 @@ import ( "time" "github.com/alessio/shellescape" + log "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/terminal" @@ -92,7 +93,7 @@ func runSSH(vm *api.VM, privKeyFile string, command []string, tty bool, timeout // of returning an error, the runSSH function defers os.Exit with the ssh // exit code. For showing any error to the user, it needs to be printed. printErrAndSetExitCode := func(errMsg error, exitCode *int, code int) error { - fmt.Printf("%v\n", errMsg) + log.Errorf("%v\n", errMsg) *exitCode = code return nil }