From 3e97a2939bd22d2af945f4e6fd99e7c79fcf2342 Mon Sep 17 00:00:00 2001 From: Yolan Romailler Date: Fri, 17 Dec 2021 20:16:49 +0100 Subject: [PATCH] Adding option to exit OTP progress bar (#2041) * RELEASE_NOTES=[ENHANCEMENT] Adding option to exit OTP progress bar Signed-off-by: Yolan Romailler * applying code review comments RELEASE_NOTES=n/a Signed-off-by: Yolan Romailler * patching CI RELEASE_NOTES=n/a Signed-off-by: Yolan Romailler * Adding a loop for OTP RELEASE_NOTES=[UX] OTP code now runs in loop until canceled or used with -o Signed-off-by: Yolan Romailler --- go.mod | 3 +- go.sum | 13 +++- internal/action/otp.go | 152 +++++++++++++++++++++++++++-------------- 3 files changed, 113 insertions(+), 55 deletions(-) diff --git a/go.mod b/go.mod index 90c0bf879f..a9cd96901b 100644 --- a/go.mod +++ b/go.mod @@ -30,12 +30,13 @@ require ( github.com/martinhoefling/goxkcdpwgen v0.0.0-20190331205820-7dc3d102eca3 github.com/mattn/go-colorable v0.1.12 github.com/mattn/go-isatty v0.0.14 + github.com/mattn/go-tty v0.0.3 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-ps v1.0.0 github.com/muesli/crunchy v0.4.0 github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 github.com/pkg/errors v0.9.1 // indirect - github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 // indirect github.com/schollz/closestmatch v0.0.0-20190308193919-1fbe626be92e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index bdf8f6dd8a..77ed5e7e2b 100644 --- a/go.sum +++ b/go.sum @@ -164,12 +164,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/martinhoefling/goxkcdpwgen v0.0.0-20190331205820-7dc3d102eca3 h1:fvQLuMSKU08pIM+I7I8pjbbPjW6Nx4sf7jOx/Pjc0qI= github.com/martinhoefling/goxkcdpwgen v0.0.0-20190331205820-7dc3d102eca3/go.mod h1:4HvZROUEazha3RDnoBcxQlwcIbQfwx035roFOMnICSE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= +github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= @@ -187,8 +193,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I= +github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -305,6 +311,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -313,6 +320,8 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/action/otp.go b/internal/action/otp.go index 3d839d0f6f..97a51abf90 100644 --- a/internal/action/otp.go +++ b/internal/action/otp.go @@ -12,6 +12,7 @@ import ( "github.com/gopasspw/gopass/pkg/otp" "github.com/gopasspw/gopass/pkg/termio" + "github.com/mattn/go-tty" "github.com/urfave/cli/v2" ) @@ -36,72 +37,119 @@ func (s *Action) OTP(c *cli.Context) error { return s.otp(ctx, name, qrf, clip, pw, true) } +func tickingBar(ctx context.Context, expiresAt time.Time, bar *termio.ProgressBar) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for tt := range ticker.C { + select { + case <-ctx.Done(): + return // returning not to leak the goroutine + default: + // we don't want to block if not cancelled + } + if tt.After(expiresAt) { + return + } + bar.Inc() + } +} + +func waitForKeyPress(ctx context.Context, cancel context.CancelFunc) { + tty, err := tty.Open() + if err != nil { + out.Errorf(ctx, "Unexpected error opening tty: %v", err) + cancel() + } + defer tty.Close() + for { + select { + case <-ctx.Done(): + return // returning not to leak the goroutine + default: + } + r, err := tty.ReadRune() + if err != nil { + out.Errorf(ctx, "Unexpected error opening tty: %v", err) + } + if r == 'q' || r == 'x' || err != nil { + cancel() + return + } + } +} + func (s *Action) otp(ctx context.Context, name, qrf string, clip, pw, recurse bool) error { sec, err := s.Store.Get(ctx, name) if err != nil { return s.otpHandleError(ctx, name, qrf, clip, pw, recurse, err) } - two, label, err := otp.Calculate(name, sec) - if err != nil { - return ExitError(ExitUnknown, err, "No OTP entry found for %s: %s", name, err) - } - token := two.OTP() - now := time.Now() - expiresAt := now.Add(otpPeriod * time.Second).Truncate(otpPeriod * time.Second) - secondsLeft := int(time.Until(expiresAt).Seconds()) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + skip := ctxutil.IsHidden(ctx) || pw || qrf != "" || out.OutputIsRedirected() + if !skip { + // let us monitor key presses for cancellation: + go waitForKeyPress(ctx, cancel) + } - if clip { - if err := clipboard.CopyTo(ctx, fmt.Sprintf("token for %s", name), []byte(token), s.cfg.ClipTimeout); err != nil { - return ExitError(ExitIO, err, "failed to copy to clipboard: %s", err) + for { + select { + case <-ctx.Done(): + return nil + default: } - } + two, label, err := otp.Calculate(name, sec) + if err != nil { + return ExitError(ExitUnknown, err, "No OTP entry found for %s: %s", name, err) + } + token := two.OTP() - done := make(chan bool) - skip := false - // check if we are in "password only" or in "qr code" mode or being redirected to a pipe - if pw || qrf != "" || out.OutputIsRedirected() { - out.Printf(ctx, "%s", token) - skip = true - } else { // if not then we want to print a progress bar with the expiry time - out.Printf(ctx, "%s", token) - out.Warningf(ctx, "This OTP password still lasts for:", nil) + now := time.Now() + expiresAt := now.Add(otpPeriod * time.Second).Truncate(otpPeriod * time.Second) + secondsLeft := int(time.Until(expiresAt).Seconds()) bar := termio.NewProgressBar(int64(secondsLeft)) - bar.Hidden = ctxutil.IsHidden(ctx) - if bar.Hidden { - skip = true - } else { - bar.Set(0) - go func() { - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - for tt := range ticker.C { - if tt.After(expiresAt) { - bar.Done() - done <- true - return - } - bar.Inc() - } - }() + bar.Hidden = skip + + if clip { + if err := clipboard.CopyTo(ctx, fmt.Sprintf("token for %s", name), []byte(token), s.cfg.ClipTimeout); err != nil { + return ExitError(ExitIO, err, "failed to copy to clipboard: %s", err) + } } - } - if qrf != "" { - return otp.WriteQRFile(two, label, qrf) - } + // check if we are in "password only" or in "qr code" mode or being redirected to a pipe + if pw || qrf != "" || out.OutputIsRedirected() { + out.Printf(ctx, "%s", token) + cancel() + } else { // if not then we want to print a progress bar with the expiry time + out.Printf(ctx, "%s", token) + out.Warningf(ctx, "([q] to stop. -o flag to avoid.) This OTP password still lasts for:", nil) - // we need to return if we are skipping, to avoid a deadlock in select - if skip { - return nil - } + if bar.Hidden { + cancel() + } else { + bar.Set(0) + go tickingBar(ctx, expiresAt, bar) + } + } - // we wait until our ticker is done or we get a cancelation - select { - case <-done: - return nil - case <-ctx.Done(): - return termio.ErrAborted + if qrf != "" { + return otp.WriteQRFile(two, label, qrf) + } + + // let us wait until next OTP code: + for { + select { + case <-ctx.Done(): + bar.Done() + return nil + default: + time.Sleep(time.Millisecond * 500) + } + if time.Now().After(expiresAt) { + bar.Done() + break + } + } } }