Skip to content

Commit 92ac13f

Browse files
FiloSottilenicdumz
andcommitted
plugin: add NewTerminalUI
Closes #611 Closes #591 Co-authored-by: Nicolas Dumazet <nicdumz.commits@gmail.com>
1 parent a623244 commit 92ac13f

File tree

7 files changed

+267
-238
lines changed

7 files changed

+267
-238
lines changed

cmd/age/age.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import (
2222
"filippo.io/age"
2323
"filippo.io/age/agessh"
2424
"filippo.io/age/armor"
25+
"filippo.io/age/internal/term"
2526
"filippo.io/age/plugin"
26-
"golang.org/x/term"
2727
)
2828

2929
const usage = `Usage:
@@ -251,7 +251,7 @@ func main() {
251251
in = f
252252
} else {
253253
stdinInUse = true
254-
if decryptFlag && term.IsTerminal(int(os.Stdin.Fd())) {
254+
if decryptFlag && term.IsTerminal(os.Stdin) {
255255
// If the input comes from a TTY, assume it's armored, and buffer up
256256
// to the END line (or EOF/EOT) so that a password prompt or the
257257
// output don't get in the way of typing the input. See Issue 364.
@@ -275,7 +275,7 @@ func main() {
275275
}
276276
}()
277277
out = f
278-
} else if term.IsTerminal(int(os.Stdout.Fd())) {
278+
} else if term.IsTerminal(os.Stdout) {
279279
if name != "-" {
280280
if decryptFlag {
281281
// TODO: buffer the output and check it's printable.
@@ -287,7 +287,7 @@ func main() {
287287
`force anyway with "-o -"`)
288288
}
289289
}
290-
if in == os.Stdin && term.IsTerminal(int(os.Stdin.Fd())) {
290+
if in == os.Stdin && term.IsTerminal(os.Stdin) {
291291
// If the input comes from a TTY and output will go to a TTY,
292292
// buffer it up so it doesn't get in the way of typing the input.
293293
buf := &bytes.Buffer{}
@@ -309,7 +309,7 @@ func main() {
309309
}
310310

311311
func passphrasePromptForEncryption() (string, error) {
312-
pass, err := readSecret("Enter passphrase (leave empty to autogenerate a secure one):")
312+
pass, err := term.ReadSecret("Enter passphrase (leave empty to autogenerate a secure one):")
313313
if err != nil {
314314
return "", fmt.Errorf("could not read passphrase: %v", err)
315315
}
@@ -325,7 +325,7 @@ func passphrasePromptForEncryption() (string, error) {
325325
return "", fmt.Errorf("could not print passphrase: %v", err)
326326
}
327327
} else {
328-
confirm, err := readSecret("Confirm passphrase:")
328+
confirm, err := term.ReadSecret("Confirm passphrase:")
329329
if err != nil {
330330
return "", fmt.Errorf("could not read passphrase: %v", err)
331331
}
@@ -370,7 +370,7 @@ func encryptNotPass(recs, files []string, identities identityFlags, in io.Reader
370370
}
371371
recipients = append(recipients, r...)
372372
case "j":
373-
id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI)
373+
id, err := plugin.NewIdentityWithoutData(f.Value, plugin.NewTerminalUI(printf, warningf))
374374
if err != nil {
375375
errorf("initializing %q: %v", f.Value, err)
376376
}
@@ -450,7 +450,7 @@ func decryptNotPass(flags identityFlags, in io.Reader, out io.Writer) {
450450
}
451451
identities = append(identities, ids...)
452452
case "j":
453-
id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI)
453+
id, err := plugin.NewIdentityWithoutData(f.Value, plugin.NewTerminalUI(printf, warningf))
454454
if err != nil {
455455
errorf("initializing %q: %v", f.Value, err)
456456
}
@@ -509,7 +509,7 @@ func decrypt(identities []age.Identity, in io.Reader, out io.Writer) {
509509
var lazyScryptIdentity = &LazyScryptIdentity{passphrasePromptForDecryption}
510510

511511
func passphrasePromptForDecryption() (string, error) {
512-
pass, err := readSecret("Enter passphrase:")
512+
pass, err := term.ReadSecret("Enter passphrase:")
513513
if err != nil {
514514
return "", fmt.Errorf("could not read passphrase: %v", err)
515515
}

cmd/age/parse.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"filippo.io/age"
1717
"filippo.io/age/agessh"
1818
"filippo.io/age/armor"
19+
"filippo.io/age/internal/term"
1920
"filippo.io/age/plugin"
2021
"filippo.io/age/tag"
2122
"golang.org/x/crypto/cryptobyte"
@@ -37,7 +38,7 @@ func parseRecipient(arg string) (age.Recipient, error) {
3738
case strings.HasPrefix(arg, "age1pq1"):
3839
return age.ParseHybridRecipient(arg)
3940
case strings.HasPrefix(arg, "age1") && strings.Count(arg, "1") > 1:
40-
return plugin.NewRecipient(arg, pluginTerminalUI)
41+
return plugin.NewRecipient(arg, plugin.NewTerminalUI(printf, warningf))
4142
case strings.HasPrefix(arg, "age1"):
4243
return age.ParseX25519Recipient(arg)
4344
case strings.HasPrefix(arg, "ssh-"):
@@ -175,7 +176,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
175176
return []age.Identity{&EncryptedIdentity{
176177
Contents: contents,
177178
Passphrase: func() (string, error) {
178-
pass, err := readSecret(fmt.Sprintf("Enter passphrase for identity file %q:", name))
179+
pass, err := term.ReadSecret(fmt.Sprintf("Enter passphrase for identity file %q:", name))
179180
if err != nil {
180181
return "", fmt.Errorf("could not read passphrase: %v", err)
181182
}
@@ -211,7 +212,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
211212
func parseIdentity(s string) (age.Identity, error) {
212213
switch {
213214
case strings.HasPrefix(s, "AGE-PLUGIN-"):
214-
return plugin.NewIdentity(s, pluginTerminalUI)
215+
return plugin.NewIdentity(s, plugin.NewTerminalUI(printf, warningf))
215216
case strings.HasPrefix(s, "AGE-SECRET-KEY-1"):
216217
return age.ParseX25519Identity(s)
217218
case strings.HasPrefix(s, "AGE-SECRET-KEY-PQ-1"):
@@ -265,7 +266,7 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
265266
}
266267
}
267268
passphrasePrompt := func() ([]byte, error) {
268-
pass, err := readSecret(fmt.Sprintf("Enter passphrase for %q:", name))
269+
pass, err := term.ReadSecret(fmt.Sprintf("Enter passphrase for %q:", name))
269270
if err != nil {
270271
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
271272
}

cmd/age/tui.go

Lines changed: 5 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ package main
1212
//
1313
// - Everything else goes to standard error with an "age:" prefix.
1414
// No capitalized initials and no periods at the end.
15+
//
16+
// The one exception is the autogenerated passphrase, which goes to
17+
// the terminal, since we really want it to reach the user only.
1518

1619
import (
1720
"bytes"
18-
"errors"
1921
"fmt"
2022
"io"
2123
"log"
2224
"os"
23-
"runtime"
2425

2526
"filippo.io/age/armor"
26-
"filippo.io/age/plugin"
27-
"golang.org/x/term"
27+
"filippo.io/age/internal/term"
2828
)
2929

3030
// l is a logger with no prefixes.
@@ -53,183 +53,13 @@ func errorWithHint(error string, hints ...string) {
5353
os.Exit(1)
5454
}
5555

56-
// avoidTerminalEscapeSequences is set if we need to avoid using escape
57-
// sequences to prevent weird characters being printed to the console. This will
58-
// happen on Windows when virtual terminal processing cannot be enabled.
59-
var avoidTerminalEscapeSequences bool
60-
61-
// clearLine clears the current line on the terminal, or opens a new line if
62-
// terminal escape codes don't work.
63-
func clearLine(out io.Writer) {
64-
const (
65-
CUI = "\033[" // Control Sequence Introducer
66-
CPL = CUI + "F" // Cursor Previous Line
67-
EL = CUI + "K" // Erase in Line
68-
)
69-
70-
// First, open a new line, which is guaranteed to work everywhere. Then, try
71-
// to erase the line above with escape codes, if possible.
72-
//
73-
// (We use CRLF instead of LF to work around an apparent bug in WSL2's
74-
// handling of CONOUT$. Only when running a Windows binary from WSL2, the
75-
// cursor would not go back to the start of the line with a simple LF.
76-
// Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)
77-
fmt.Fprintf(out, "\r\n")
78-
if !avoidTerminalEscapeSequences {
79-
fmt.Fprintf(out, CPL+EL)
80-
}
81-
}
82-
83-
// withTerminal runs f with the terminal input and output files, if available.
84-
// withTerminal does not open a non-terminal stdin, so the caller does not need
85-
// to check stdinInUse.
86-
func withTerminal(f func(in, out *os.File) error) error {
87-
if runtime.GOOS == "windows" {
88-
in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
89-
if err != nil {
90-
return err
91-
}
92-
defer in.Close()
93-
out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
94-
if err != nil {
95-
return err
96-
}
97-
defer out.Close()
98-
return f(in, out)
99-
} else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
100-
defer tty.Close()
101-
return f(tty, tty)
102-
} else if term.IsTerminal(int(os.Stdin.Fd())) {
103-
return f(os.Stdin, os.Stdin)
104-
} else {
105-
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
106-
}
107-
}
108-
10956
func printfToTerminal(format string, v ...interface{}) error {
110-
return withTerminal(func(_, out *os.File) error {
57+
return term.WithTerminal(func(_, out *os.File) error {
11158
_, err := fmt.Fprintf(out, "age: "+format+"\n", v...)
11259
return err
11360
})
11461
}
11562

116-
// readSecret reads a value from the terminal with no echo. The prompt is ephemeral.
117-
func readSecret(prompt string) (s []byte, err error) {
118-
err = withTerminal(func(in, out *os.File) error {
119-
fmt.Fprintf(out, "%s ", prompt)
120-
defer clearLine(out)
121-
s, err = term.ReadPassword(int(in.Fd()))
122-
return err
123-
})
124-
return
125-
}
126-
127-
// readPublic reads a value from the terminal. The prompt is ephemeral.
128-
func readPublic(prompt string) (s []byte, err error) {
129-
err = withTerminal(func(in, out *os.File) error {
130-
fmt.Fprintf(out, "%s ", prompt)
131-
defer clearLine(out)
132-
133-
oldState, err := term.MakeRaw(int(in.Fd()))
134-
if err != nil {
135-
return err
136-
}
137-
defer term.Restore(int(in.Fd()), oldState)
138-
139-
t := term.NewTerminal(in, "")
140-
line, err := t.ReadLine()
141-
s = []byte(line)
142-
return err
143-
})
144-
return
145-
}
146-
147-
// readCharacter reads a single character from the terminal with no echo. The
148-
// prompt is ephemeral.
149-
func readCharacter(prompt string) (c byte, err error) {
150-
err = withTerminal(func(in, out *os.File) error {
151-
fmt.Fprintf(out, "%s ", prompt)
152-
defer clearLine(out)
153-
154-
oldState, err := term.MakeRaw(int(in.Fd()))
155-
if err != nil {
156-
return err
157-
}
158-
defer term.Restore(int(in.Fd()), oldState)
159-
160-
b := make([]byte, 1)
161-
if _, err := in.Read(b); err != nil {
162-
return err
163-
}
164-
165-
c = b[0]
166-
return nil
167-
})
168-
return
169-
}
170-
171-
var pluginTerminalUI = &plugin.ClientUI{
172-
DisplayMessage: func(name, message string) error {
173-
printf("%s plugin: %s", name, message)
174-
return nil
175-
},
176-
RequestValue: func(name, message string, isSecret bool) (s string, err error) {
177-
defer func() {
178-
if err != nil {
179-
warningf("could not read value for age-plugin-%s: %v", name, err)
180-
}
181-
}()
182-
if isSecret {
183-
secret, err := readSecret(message)
184-
if err != nil {
185-
return "", err
186-
}
187-
return string(secret), nil
188-
} else {
189-
public, err := readPublic(message)
190-
if err != nil {
191-
return "", err
192-
}
193-
return string(public), nil
194-
}
195-
},
196-
Confirm: func(name, message, yes, no string) (choseYes bool, err error) {
197-
defer func() {
198-
if err != nil {
199-
warningf("could not read value for age-plugin-%s: %v", name, err)
200-
}
201-
}()
202-
if no == "" {
203-
message += fmt.Sprintf(" (press enter for %q)", yes)
204-
_, err := readSecret(message)
205-
if err != nil {
206-
return false, err
207-
}
208-
return true, nil
209-
}
210-
message += fmt.Sprintf(" (press [1] for %q or [2] for %q)", yes, no)
211-
for {
212-
selection, err := readCharacter(message)
213-
if err != nil {
214-
return false, err
215-
}
216-
switch selection {
217-
case '1':
218-
return true, nil
219-
case '2':
220-
return false, nil
221-
case '\x03': // CTRL-C
222-
return false, errors.New("user cancelled prompt")
223-
default:
224-
warningf("reading value for age-plugin-%s: invalid selection %q", name, selection)
225-
}
226-
}
227-
},
228-
WaitTimer: func(name string) {
229-
printf("waiting on %s plugin...", name)
230-
},
231-
}
232-
23363
func bufferTerminalInput(in io.Reader) (io.Reader, error) {
23464
buf := &bytes.Buffer{}
23565
if _, err := buf.ReadFrom(ReaderFunc(func(p []byte) (n int, err error) {

cmd/age/tui_windows.go

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)