cmd/gitbay/stdinpayload.go

c72da83f1a127ce5d5310bddea7240344b0dc11f
gitbay/cmd/gitbay/stdinpayload.go history · blame · raw

47 lines · 1715 bytes

 1package main
 2
 3import (
 4	"bytes"
 5	"fmt"
 6	"io"
 7	"os"
 8
 9	"golang.org/x/term"
10)
11
12// A command whose payload is stdin blocks on a terminal with nothing
13// printed, which is indistinguishable from a hung connection — and
14// pressing Enter does not end it, because the server reads to EOF. So it
15// looks the same before and after you have done the right thing (#150).
16//
17// The fix belongs here rather than in the daemon: whether stdin is a
18// terminal is a fact about the client, and the server cannot see it.
19
20// isTerminal is a variable so tests can drive both paths; a test process
21// has no terminal, which is the case that must stay byte-identical.
22var isTerminal = func(f *os.File) bool { return term.IsTerminal(int(f.Fd())) }
23
24// stdinPayload returns the reader for a command that takes its payload on
25// stdin. Piped or redirected input is passed through untouched — no
26// prompt, no added or removed bytes — because a script's `printf %s
27// "$TOKEN" | gitbay ...` must send exactly the token.
28//
29// what names the thing being read, for the prompt. secret hides the input
30// and takes a single line, so a credential never lands in the terminal's
31// scrollback and Enter is enough to finish.
32func stdinPayload(in *os.File, what string, secret bool) (io.Reader, error) {
33	if !isTerminal(in) {
34		return in, nil
35	}
36	if secret {
37		fmt.Fprintf(os.Stderr, "%s (input hidden, Enter when done): ", what)
38		raw, err := term.ReadPassword(int(in.Fd()))
39		fmt.Fprintln(os.Stderr)
40		if err != nil {
41			return nil, fmt.Errorf("reading %s: %w", what, err)
42		}
43		return bytes.NewReader(raw), nil
44	}
45	fmt.Fprintf(os.Stderr, "reading %s from stdin — paste it, then press Ctrl-D. (Or pipe it in.)\n", what)
46	return in, nil
47}