Skip to content

Commit 8913e33

Browse files
committed
remove speakeasy dependency; implement input recapture
1 parent 6e0e29a commit 8913e33

File tree

7 files changed

+82
-21
lines changed

7 files changed

+82
-21
lines changed

cli/cliui/prompt.go

+49-3
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99
"os/signal"
1010
"strings"
1111

12-
"github.com/bgentry/speakeasy"
1312
"github.com/mattn/go-isatty"
13+
"golang.org/x/term"
1414
"golang.org/x/xerrors"
1515

1616
"github.com/coder/pretty"
@@ -22,6 +22,7 @@ type PromptOptions struct {
2222
Text string
2323
Default string
2424
Secret bool
25+
Mask rune
2526
IsConfirm bool
2627
Validate func(string) error
2728
}
@@ -90,8 +91,53 @@ func Prompt(inv *serpent.Invocation, opts PromptOptions) (string, error) {
9091

9192
inFile, isInputFile := inv.Stdin.(*os.File)
9293
if opts.Secret && isInputFile && isatty.IsTerminal(inFile.Fd()) {
93-
// we don't install a signal handler here because speakeasy has its own
94-
line, err = speakeasy.Ask("")
94+
// Set terminal to raw mode
95+
oldState, err := term.MakeRaw(int(inFile.Fd()))
96+
if err != nil {
97+
errCh <- err
98+
return
99+
}
100+
defer func() {
101+
_ = term.Restore(int(inFile.Fd()), oldState)
102+
}()
103+
104+
// Read input character by character
105+
buf := make([]byte, 1)
106+
for {
107+
n, err := inv.Stdin.Read(buf)
108+
if err != nil || n == 0 {
109+
break
110+
}
111+
112+
// Handle special characters
113+
switch buf[0] {
114+
case '\r', '\n': // Enter
115+
_, _ = fmt.Fprint(inv.Stdout, "\n")
116+
lineCh <- line
117+
return
118+
case 3: // Ctrl+C
119+
_, _ = fmt.Fprint(inv.Stdout, "\n")
120+
errCh <- ErrCanceled
121+
return
122+
case 8, 127: // Backspace/Delete
123+
if len(line) > 0 {
124+
line = line[:len(line)-1]
125+
// Move cursor back, print space, move cursor back again
126+
_, _ = fmt.Fprint(inv.Stdout, "\b \b")
127+
}
128+
default:
129+
// Only append printable characters
130+
if buf[0] >= 32 && buf[0] <= 126 {
131+
line += string(buf[0])
132+
// Print the mask character
133+
if opts.Mask == 0 {
134+
_, _ = fmt.Fprint(inv.Stdout, "")
135+
} else {
136+
_, _ = fmt.Fprint(inv.Stdout, string(opts.Mask))
137+
}
138+
}
139+
}
140+
}
95141
} else {
96142
signal.Notify(interrupt, os.Interrupt)
97143
defer signal.Stop(interrupt)

cli/cliui/prompt_test.go

+29-15
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"golang.org/x/xerrors"
1414

1515
"github.com/coder/coder/v2/cli/cliui"
16-
"github.com/coder/coder/v2/pty"
1716
"github.com/coder/coder/v2/pty/ptytest"
1817
"github.com/coder/coder/v2/testutil"
1918
"github.com/coder/serpent"
@@ -181,6 +180,28 @@ func TestPrompt(t *testing.T) {
181180
resp := testutil.TryReceive(ctx, t, doneChan)
182181
require.Equal(t, "valid", resp)
183182
})
183+
184+
t.Run("MaskedSecret", func(t *testing.T) {
185+
t.Parallel()
186+
ctx := testutil.Context(t, testutil.WaitShort)
187+
ptty := ptytest.New(t)
188+
doneChan := make(chan string)
189+
go func() {
190+
resp, err := newPrompt(ctx, ptty, cliui.PromptOptions{
191+
Text: "Password:",
192+
Secret: true,
193+
Mask: '*',
194+
}, nil)
195+
assert.NoError(t, err)
196+
doneChan <- resp
197+
}()
198+
ptty.ExpectMatch("Password: ")
199+
200+
ptty.WriteLine("test")
201+
202+
resp := testutil.TryReceive(ctx, t, doneChan)
203+
require.Equal(t, "test", resp)
204+
})
184205
}
185206

186207
func newPrompt(ctx context.Context, ptty *ptytest.PTY, opts cliui.PromptOptions, invOpt func(inv *serpent.Invocation)) (string, error) {
@@ -212,10 +233,6 @@ func TestPasswordTerminalState(t *testing.T) {
212233
t.Parallel()
213234

214235
ptty := ptytest.New(t)
215-
ptyWithFlags, ok := ptty.PTY.(pty.WithFlags)
216-
if !ok {
217-
t.Skip("unable to check PTY local echo on this platform")
218-
}
219236

220237
cmd := exec.Command(os.Args[0], "-test.run=TestPasswordTerminalState") //nolint:gosec
221238
cmd.Env = append(os.Environ(), "TEST_SUBPROCESS=1")
@@ -229,21 +246,17 @@ func TestPasswordTerminalState(t *testing.T) {
229246
defer process.Kill()
230247

231248
ptty.ExpectMatch("Password: ")
232-
233-
require.Eventually(t, func() bool {
234-
echo, err := ptyWithFlags.EchoEnabled()
235-
return err == nil && !echo
236-
}, testutil.WaitShort, testutil.IntervalMedium, "echo is on while reading password")
249+
ptty.Write('t')
250+
ptty.Write('e')
251+
ptty.Write('s')
252+
ptty.Write('t')
253+
ptty.Write('\b')
254+
ptty.ExpectMatch("***")
237255

238256
err = process.Signal(os.Interrupt)
239257
require.NoError(t, err)
240258
_, err = process.Wait()
241259
require.NoError(t, err)
242-
243-
require.Eventually(t, func() bool {
244-
echo, err := ptyWithFlags.EchoEnabled()
245-
return err == nil && echo
246-
}, testutil.WaitShort, testutil.IntervalMedium, "echo is off after reading password")
247260
}
248261

249262
// nolint:unused
@@ -253,6 +266,7 @@ func passwordHelper() {
253266
cliui.Prompt(inv, cliui.PromptOptions{
254267
Text: "Password:",
255268
Secret: true,
269+
Mask: '*',
256270
})
257271
return nil
258272
},

cli/login.go

+1
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ func (r *RootCmd) login() *serpent.Command {
356356
sessionToken, err = cliui.Prompt(inv, cliui.PromptOptions{
357357
Text: "Paste your token here:",
358358
Secret: true,
359+
Mask: '*',
359360
Validate: func(token string) error {
360361
client.SetSessionToken(token)
361362
_, err := client.User(ctx, codersdk.Me)

cli/server_createadminuser.go

+2
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
136136
newUserPassword, err = cliui.Prompt(inv, cliui.PromptOptions{
137137
Text: "Password",
138138
Secret: true,
139+
Mask: '*',
139140
Validate: func(val string) error {
140141
if val == "" {
141142
return xerrors.New("password cannot be empty")
@@ -151,6 +152,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
151152
_, err = cliui.Prompt(inv, cliui.PromptOptions{
152153
Text: "Confirm password",
153154
Secret: true,
155+
Mask: '*',
154156
Validate: func(val string) error {
155157
if val != newUserPassword {
156158
return xerrors.New("passwords do not match")

cmd/cliui/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ func main() {
109109
_, err = cliui.Prompt(inv, cliui.PromptOptions{
110110
Text: "Enter password",
111111
Secret: true,
112+
Mask: '*',
112113
})
113114
return err
114115
},

go.mod

-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ require (
8383
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
8484
github.com/awalterschulze/gographviz v2.0.3+incompatible
8585
github.com/aws/smithy-go v1.22.3
86-
github.com/bgentry/speakeasy v0.2.0
8786
github.com/bramvdbogaerde/go-scp v1.5.0
8887
github.com/briandowns/spinner v1.23.0
8988
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5

go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -815,8 +815,6 @@ github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
815815
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
816816
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
817817
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
818-
github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
819-
github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
820818
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
821819
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
822820
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=

0 commit comments

Comments
 (0)