Skip to content
This repository has been archived by the owner on Sep 20, 2021. It is now read-only.

Commit

Permalink
Remove os variation; use ssh/terminal for necessary terminal calls
Browse files Browse the repository at this point in the history
 - The golang/crypto/ssh/terminal package properly makes terminals
raw on bsd, linux, and windows.  There is no need to make the
syscalls ourselves now for terminal handling.
 - The ReadFile syscall on Windows is just the underlying
implementation for *File.Read(); using the higher level calls
allows all OSs to share an implementation. The same goes for
the *nix system calls.  File reading can be done centrally as
well.
 - Confirmed tests pass on OSX and Windows manually.
 - Moved terminal handling into the getPasswd method now that
it is not OS dependend to remove unnecessary calls.
 - Fixes an issue where gopass would fail to pipe input properly due
to inability to make raw.  Now tests check that os.Stdin is a terminal
before trying to make raw.
 - Adds tests to ensure proper pipe behavior
 - Modifies travis to test on linux and osx
  • Loading branch information
johnSchnake committed Mar 2, 2016
1 parent c89a328 commit e7699d3
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 86 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
@@ -1,5 +1,9 @@
language: go

os:
- linux
- osx

go:
- 1.3
- 1.4
Expand Down
2 changes: 1 addition & 1 deletion README.md
@@ -1,6 +1,6 @@
# getpasswd in Go [![GoDoc](https://godoc.org/github.com/howeyc/gopass?status.svg)](https://godoc.org/github.com/howeyc/gopass) [![Build Status](https://secure.travis-ci.org/howeyc/gopass.png?branch=master)](http://travis-ci.org/howeyc/gopass)

Retrieve password from user terminal input without echo
Retrieve password from user terminal or piped input without echo.

Verified on BSD, Linux, and Windows.

Expand Down
27 changes: 0 additions & 27 deletions nix.go

This file was deleted.

26 changes: 26 additions & 0 deletions pass.go
Expand Up @@ -3,10 +3,28 @@ package gopass
import (
"errors"
"fmt"
"io"
"os"

"golang.org/x/crypto/ssh/terminal"
)

var defaultGetCh = func() (byte, error) {
buf := make([]byte, 1)
if n, err := os.Stdin.Read(buf); n == 0 || err != nil {
if err != nil {
return 0, err
}
return 0, io.EOF
}
return buf[0], nil
}

var (
ErrInterrupted = errors.New("Interrupted")

// Provide variable so that tests can provide a mock implementation.
getch = defaultGetCh
)

// getPasswd returns the input read from terminal.
Expand All @@ -20,6 +38,14 @@ func getPasswd(masked bool) ([]byte, error) {
mask = []byte("*")
}

if terminal.IsTerminal(int(os.Stdin.Fd())) {
if oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())); err != nil {
return pass, err
} else {
defer terminal.Restore(int(os.Stdin.Fd()), oldState)
}
}

for {
if v, e := getch(); v == 127 || v == 8 {
if l := len(pass); l > 0 {
Expand Down
102 changes: 89 additions & 13 deletions pass_test.go
@@ -1,10 +1,14 @@
package gopass

import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"testing"
"time"
)

// TestGetPasswd tests the password creation and output based on a byte buffer
Expand Down Expand Up @@ -40,22 +44,12 @@ func TestGetPasswd(t *testing.T) {
"Nil byte should be ignored due; may get unintended nil bytes from syscalls on Windows."},
}

// getch methods normally refer to syscalls; replace with a byte buffer.
var input *bytes.Buffer
getch = func() (byte, error) {
b, err := input.ReadByte()
if err != nil {
t.Fatal(err.Error())
}
return b, nil
}

// Redirecting output for tests as they print to os.Stdout but we want to
// capture and test the output.
origStdOut := os.Stdout
for _, masked := range []bool{true, false} {
for _, d := range ds {
input = bytes.NewBuffer(d.input)
pipeBytesToStdin(d.input)

r, w, err := os.Pipe()
if err != nil {
Expand All @@ -68,6 +62,7 @@ func TestGetPasswd(t *testing.T) {
if err != nil {
t.Errorf("Error getting password:", err.Error())
}
leftOnBuffer := flushStdin()

// Test output (masked and unmasked). Delete/backspace actually
// deletes, overwrites and deletes again. As a result, we need to
Expand All @@ -92,9 +87,90 @@ func TestGetPasswd(t *testing.T) {
t.Errorf("Expected %q but got %q instead when masked=%v. %s", d.password, result, masked, d.reason)
}

if input.Len() != d.byesLeft {
t.Errorf("Expected %v bytes left on buffer but instead got %v when masked=%v. %s", d.byesLeft, input.Len(), masked, d.reason)
if leftOnBuffer != d.byesLeft {
t.Errorf("Expected %v bytes left on buffer but instead got %v when masked=%v. %s", d.byesLeft, leftOnBuffer, masked, d.reason)
}
}
}
}

// TestPipe ensures we get our expected pipe behavior.
func TestPipe(t *testing.T) {
type testData struct {
input string
password string
expError error
}
ds := []testData{
testData{"abc", "abc", io.EOF},
testData{"abc\n", "abc", nil},
testData{"abc\r", "abc", nil},
testData{"abc\r\n", "abc", nil},
}

for _, d := range ds {
_, err := pipeToStdin(d.input)
if err != nil {
t.Log("Error writing input to stdin:", err)
t.FailNow()
}
pass, err := GetPasswd()
if string(pass) != d.password {
t.Errorf("Expected %q but got %q instead.", d.password, string(pass))
}
if err != d.expError {
t.Errorf("Expected %v but got %q instead.", d.expError, err)
}
}
}

// flushStdin reads from stdin for .5 seconds to ensure no bytes are left on
// the buffer. Returns the number of bytes read.
func flushStdin() int {
ch := make(chan byte)
go func(ch chan byte) {
reader := bufio.NewReader(os.Stdin)
for {
b, err := reader.ReadByte()
if err != nil { // Maybe log non io.EOF errors, if you want
close(ch)
return
}
ch <- b
}
close(ch)
}(ch)

numBytes := 0
for {
select {
case _, ok := <-ch:
if !ok {
return numBytes
}
numBytes++
case <-time.After(500 * time.Millisecond):
return numBytes
}
}
return numBytes
}

// pipeToStdin pipes the given string onto os.Stdin by replacing it with an
// os.Pipe. The write end of the pipe is closed so that EOF is read after the
// final byte.
func pipeToStdin(s string) (int, error) {
pipeReader, pipeWriter, err := os.Pipe()
if err != nil {
fmt.Println("Error getting os pipes:", err)
os.Exit(1)
}
os.Stdin = pipeReader
w, err := pipeWriter.WriteString(s)
pipeWriter.Close()
return w, err
}

func pipeBytesToStdin(b []byte) (int, error) {
return pipeToStdin(string(b))
}
45 changes: 0 additions & 45 deletions win.go

This file was deleted.

0 comments on commit e7699d3

Please sign in to comment.