Skip to content

Commit f0b5db6

Browse files
authored
Merge pull request #2034 from felixfontein/age-term
Use age's plugin.NewTerminalUI() instead of vendoring the code
2 parents ef25473 + b6e1224 commit f0b5db6

3 files changed

Lines changed: 13 additions & 152 deletions

File tree

age/encrypted_keys.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,7 @@ func unwrapIdentities(location string, reader io.Reader) (ParsedIdentities, erro
145145
Passphrase: func() (string, error) {
146146
conn, err := gpgagent.NewConn()
147147
if err != nil {
148-
passphrase, err := readSecret(fmt.Sprintf("Enter passphrase for identity '%s':", location))
149-
if err != nil {
150-
return "", err
151-
}
152-
return string(passphrase), nil
148+
return pluginTerminalUI.RequestValue("", fmt.Sprintf("Enter passphrase for identity '%s':", location), true)
153149
}
154150
defer func(conn *gpgagent.Conn) {
155151
if err := conn.Close(); err != nil {

age/ssh_parse.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ func parseSSHIdentityFromPrivateKeyFile(keyPath string) (age.Identity, error) {
6565
}
6666
}
6767
passphrasePrompt := func() ([]byte, error) {
68-
pass, err := readSecret(fmt.Sprintf("Enter passphrase for %q:", keyPath))
68+
pass, err := pluginTerminalUI.RequestValue("", fmt.Sprintf("Enter passphrase for %q:", keyPath), true)
6969
if err != nil {
7070
return nil, fmt.Errorf("could not read passphrase for %q: %v", keyPath, err)
7171
}
72-
return pass, nil
72+
return []byte(pass), nil
7373
}
7474
i, err := agessh.NewEncryptedSSHIdentity(pubKey, contents, passphrasePrompt)
7575
if err != nil {

age/tui.go

Lines changed: 10 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,8 @@
1-
// These functions have been copied from the age project
2-
// https://github.com/FiloSottile/age/blob/3d91014ea095e8d70f7c6c4833f89b53a96e0832/cmd/age/tui.go
3-
//
4-
// Copyright 2021 The age Authors. All rights reserved.
5-
// Use of this source code is governed by a BSD-style
6-
// license that can be found in age's LICENSE file at
7-
// https://github.com/FiloSottile/age/blob/v1.0.0/LICENSE
8-
//
9-
// SPDX-License-Identifier: BSD-3-Clause
10-
111
package age
122

133
import (
14-
"errors"
154
"filippo.io/age/plugin"
16-
"fmt"
17-
"io"
18-
"os"
19-
"runtime"
205
"testing"
21-
22-
"golang.org/x/term"
236
)
247

258
var testOnlyAgePassword string
@@ -32,142 +15,24 @@ func warningf(format string, v ...interface{}) {
3215
log.Printf("age: warning: "+format, v...)
3316
}
3417

35-
// clearLine clears the current line on the terminal, or opens a new line if
36-
// terminal escape codes don't work.
37-
func clearLine(out io.Writer) {
38-
const (
39-
CUI = "\033[" // Control Sequence Introducer
40-
CPL = CUI + "F" // Cursor Previous Line
41-
EL = CUI + "K" // Erase in Line
42-
)
43-
44-
// First, open a new line, which is guaranteed to work everywhere. Then, try
45-
// to erase the line above with escape codes.
46-
//
47-
// (We use CRLF instead of LF to work around an apparent bug in WSL2's
48-
// handling of CONOUT$. Only when running a Windows binary from WSL2, the
49-
// cursor would not go back to the start of the line with a simple LF.
50-
// Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)
51-
fmt.Fprintf(out, "\r\n"+CPL+EL)
52-
}
53-
54-
// withTerminal runs f with the terminal input and output files, if available.
55-
// withTerminal does not open a non-terminal stdin, so the caller does not need
56-
// to check stdinInUse.
57-
func withTerminal(f func(in, out *os.File) error) error {
58-
if runtime.GOOS == "windows" {
59-
in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
60-
if err != nil {
61-
return err
62-
}
63-
defer in.Close()
64-
out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
65-
if err != nil {
66-
return err
67-
}
68-
defer out.Close()
69-
return f(in, out)
70-
} else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
71-
defer tty.Close()
72-
return f(tty, tty)
73-
} else if term.IsTerminal(int(os.Stdin.Fd())) {
74-
return f(os.Stdin, os.Stdin)
75-
} else {
76-
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
77-
}
78-
}
79-
80-
// readSecret reads a value from the terminal with no echo. The prompt is ephemeral.
81-
func readSecret(prompt string) (s []byte, err error) {
82-
if testing.Testing() {
83-
if testOnlyAgePassword != "" {
84-
return []byte(testOnlyAgePassword), nil
85-
}
86-
}
87-
88-
err = withTerminal(func(in, out *os.File) error {
89-
fmt.Fprintf(out, "%s ", prompt)
90-
defer clearLine(out)
91-
s, err = term.ReadPassword(int(in.Fd()))
92-
return err
93-
})
94-
return
95-
}
96-
97-
// readCharacter reads a single character from the terminal with no echo. The
98-
// prompt is ephemeral.
99-
func readCharacter(prompt string) (c byte, err error) {
100-
err = withTerminal(func(in, out *os.File) error {
101-
fmt.Fprintf(out, "%s ", prompt)
102-
defer clearLine(out)
103-
104-
oldState, err := term.MakeRaw(int(in.Fd()))
105-
if err != nil {
106-
return err
107-
}
108-
defer term.Restore(int(in.Fd()), oldState)
109-
110-
b := make([]byte, 1)
111-
if _, err := in.Read(b); err != nil {
112-
return err
113-
}
114-
115-
c = b[0]
116-
return nil
117-
})
118-
return
119-
}
18+
var pluginTerminalUIImpl = plugin.NewTerminalUI(printf, warningf)
12019

20+
// We cannot use plugin.NewTerminalUI() directly because we want to be able to
21+
// inject specific return values for RequestValue during testing.
12122
var pluginTerminalUI = &plugin.ClientUI{
12223
DisplayMessage: func(name, message string) error {
123-
printf("%s plugin: %s", name, message)
124-
return nil
24+
return pluginTerminalUIImpl.DisplayMessage(name, message)
12525
},
126-
RequestValue: func(name, message string, _ bool) (s string, err error) {
127-
defer func() {
128-
if err != nil {
129-
warningf("could not read value for age-plugin-%s: %v", name, err)
130-
}
131-
}()
132-
secret, err := readSecret(message)
133-
if err != nil {
134-
return "", err
26+
RequestValue: func(name, message string, isSecret bool) (s string, err error) {
27+
if testing.Testing() && testOnlyAgePassword != "" {
28+
return testOnlyAgePassword, nil
13529
}
136-
return string(secret), nil
30+
return pluginTerminalUIImpl.RequestValue(name, message, isSecret);
13731
},
13832
Confirm: func(name, message, yes, no string) (choseYes bool, err error) {
139-
defer func() {
140-
if err != nil {
141-
warningf("could not read value for age-plugin-%s: %v", name, err)
142-
}
143-
}()
144-
if no == "" {
145-
message += fmt.Sprintf(" (press enter for %q)", yes)
146-
_, err := readSecret(message)
147-
if err != nil {
148-
return false, err
149-
}
150-
return true, nil
151-
}
152-
message += fmt.Sprintf(" (press [1] for %q or [2] for %q)", yes, no)
153-
for {
154-
selection, err := readCharacter(message)
155-
if err != nil {
156-
return false, err
157-
}
158-
switch selection {
159-
case '1':
160-
return true, nil
161-
case '2':
162-
return false, nil
163-
case '\x03': // CTRL-C
164-
return false, errors.New("user cancelled prompt")
165-
default:
166-
warningf("reading value for age-plugin-%s: invalid selection %q", name, selection)
167-
}
168-
}
33+
return pluginTerminalUIImpl.Confirm(name, message, yes, no)
16934
},
17035
WaitTimer: func(name string) {
171-
printf("waiting on %s plugin...", name)
36+
pluginTerminalUIImpl.WaitTimer(name)
17237
},
17338
}

0 commit comments

Comments
 (0)