cmd/gitbay/stdinpayload.go
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}