lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 3bcbae6565b4724264dfa7141a01afe1aef21efb
parent f6d427dc04c526bf59c34283c2128854a6a20a75
Author: Sean Enck <sean@ttypty.com>
Date:   Sat, 16 Jul 2022 15:12:32 -0400

moving clipboard to platform area

Diffstat:
Mcmd/lb-totp/main.go | 6+++---
Mcmd/lb/main.go | 8++++----
Mcontrib/completions.bash | 4++--
Dinternal/clip/clipboard.go | 142-------------------------------------------------------------------------------
Dinternal/clip/clipboard_test.go | 71-----------------------------------------------------------------------
Minternal/inputs/env.go | 5+++++
Ainternal/platform/clipboard.go | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/platform/clipboard_test.go | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/platform/core.go | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/platform/core_test.go | 27+++++++++++++++++++++++++++
10 files changed, 302 insertions(+), 222 deletions(-)

diff --git a/cmd/lb-totp/main.go b/cmd/lb-totp/main.go @@ -11,11 +11,11 @@ import ( "time" "github.com/enckse/lockbox/internal/cli" - "github.com/enckse/lockbox/internal/clip" "github.com/enckse/lockbox/internal/colors" "github.com/enckse/lockbox/internal/encrypt" "github.com/enckse/lockbox/internal/inputs" "github.com/enckse/lockbox/internal/misc" + "github.com/enckse/lockbox/internal/platform" "github.com/enckse/lockbox/internal/store" otp "github.com/pquerna/otp/totp" ) @@ -107,9 +107,9 @@ func display(token string, args cli.Arguments) error { clear() } } - clipboard := clip.Commands{} + clipboard := platform.Clipboard{} if args.Clip { - clipboard, err = clip.NewCommands() + clipboard, err = platform.NewClipboard() if err != nil { misc.Die("invalid clipboard", err) } diff --git a/cmd/lb/main.go b/cmd/lb/main.go @@ -11,13 +11,13 @@ import ( "time" "github.com/enckse/lockbox/internal/cli" - "github.com/enckse/lockbox/internal/clip" "github.com/enckse/lockbox/internal/colors" "github.com/enckse/lockbox/internal/dump" "github.com/enckse/lockbox/internal/encrypt" "github.com/enckse/lockbox/internal/hooks" "github.com/enckse/lockbox/internal/inputs" "github.com/enckse/lockbox/internal/misc" + "github.com/enckse/lockbox/internal/platform" "github.com/enckse/lockbox/internal/store" ) @@ -181,9 +181,9 @@ func main() { misc.Die("unable to get color for terminal", err) } dumpData := []dump.ExportEntity{} - clipboard := clip.Commands{} + clipboard := platform.Clipboard{} if !isShow { - clipboard, err = clip.NewCommands() + clipboard, err = platform.NewClipboard() if err != nil { misc.Die("unable to get clipboard", err) } @@ -239,7 +239,7 @@ func main() { if err != nil { misc.Die("unable to read value to clear", err) } - clipboard, err := clip.NewCommands() + clipboard, err := platform.NewClipboard() if err != nil { misc.Die("unable to get paste command", err) } diff --git a/contrib/completions.bash b/contrib/completions.bash @@ -11,8 +11,8 @@ _is_clip() { _lb() { local cur opts clip_enabled needs clip_enabled=" -c clip" - if [ -n "$LOCKBOX_CLIPMODE" ]; then - if [ "$LOCKBOX_CLIPMODE" == "off" ]; then + if [ -n "$LOCKBOX_NOCLIP" ]; then + if [ "$LOCKBOX_NOCLIP" == "yes" ]; then clip_enabled="" fi fi diff --git a/internal/clip/clipboard.go b/internal/clip/clipboard.go @@ -1,142 +0,0 @@ -package clip - -import ( - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - - "github.com/enckse/lockbox/internal/misc" -) - -const ( - maxTime = 45 - pbClipMode = "pb" - waylandClipMode = "wayland" - xClipMode = "x11" - wslMode = "wsl" -) - -type ( - // Commands represent system clipboard operations. - Commands struct { - copying []string - pasting []string - MaxTime int - } -) - -// NewCommands will retrieve the commands to use for clipboard operations. -func NewCommands() (Commands, error) { - env := strings.TrimSpace(os.Getenv("LOCKBOX_CLIPMODE")) - if env == "" { - b, err := exec.Command("uname", "-a").Output() - if err != nil { - return Commands{}, err - } - raw := strings.TrimSpace(string(b)) - parts := strings.Split(raw, " ") - switch parts[0] { - case "Darwin": - env = pbClipMode - case "Linux": - if strings.Contains(raw, "microsoft-standard-WSL2") { - env = wslMode - } else { - if strings.TrimSpace(os.Getenv("WAYLAND_DISPLAY")) == "" { - if strings.TrimSpace(os.Getenv("DISPLAY")) == "" { - return Commands{}, errors.New("unable to detect linux clipboard mode") - } - env = xClipMode - } else { - env = waylandClipMode - } - } - default: - return Commands{}, errors.New("unable to detect clipboard mode") - } - } - max := maxTime - useMax := os.Getenv("LOCKBOX_CLIPMAX") - if useMax != "" { - i, err := strconv.Atoi(useMax) - if err != nil { - return Commands{}, err - } - if i < 1 { - return Commands{}, errors.New("clipboard max time must be greater than 0") - } - max = i - } - var copying []string - var pasting []string - switch env { - case pbClipMode: - copying = []string{"pbcopy"} - pasting = []string{"pbpaste"} - case xClipMode: - copying = []string{"xclip"} - pasting = []string{"xclip", "-o"} - case waylandClipMode: - copying = []string{"wl-copy"} - pasting = []string{"wl-paste"} - case wslMode: - copying = []string{"clip.exe"} - pasting = []string{"powershell.exe", "-command", "Get-Clipboard"} - default: - return Commands{}, errors.New("clipboard is unavailable") - } - return Commands{copying: copying, pasting: pasting, MaxTime: max}, nil -} - -// CopyTo will copy to clipboard, if non-empty will clear later. -func (c Commands) CopyTo(value, executable string) { - cmd, args := c.Args(true) - pipeTo(cmd, value, true, args...) - if value != "" { - fmt.Printf("clipboard will clear in %d seconds\n", c.MaxTime) - pipeTo(filepath.Join(filepath.Dir(executable), "lb"), value, false, "clear") - } -} - -// Args returns clipboard args for execution. -func (c Commands) Args(copying bool) (string, []string) { - var using []string - if copying { - using = c.copying - } else { - using = c.pasting - } - var args []string - if len(using) > 1 { - args = using[1:] - } - return using[0], args -} - -func pipeTo(command, value string, wait bool, args ...string) { - cmd := exec.Command(command, args...) - stdin, err := cmd.StdinPipe() - if err != nil { - misc.Die("unable to get stdin pipe", err) - } - - go func() { - defer stdin.Close() - if _, err := stdin.Write([]byte(value)); err != nil { - fmt.Printf("failed writing to stdin: %v\n", err) - } - }() - var ran error - if wait { - ran = cmd.Run() - } else { - ran = cmd.Start() - } - if ran != nil { - misc.Die("failed to run command", ran) - } -} diff --git a/internal/clip/clipboard_test.go b/internal/clip/clipboard_test.go @@ -1,71 +0,0 @@ -package clip - -import ( - "os" - "testing" -) - -func TestNoClipboard(t *testing.T) { - os.Setenv("LOCKBOX_CLIPMAX", "") - os.Setenv("LOCKBOX_CLIPMODE", "off") - _, err := NewCommands() - if err == nil || err.Error() != "clipboard is unavailable" { - t.Errorf("invalid error: %v", err) - } -} - -func TestMaxTime(t *testing.T) { - os.Setenv("LOCKBOX_CLIPMODE", pbClipMode) - os.Setenv("LOCKBOX_CLIPMAX", "") - c, err := NewCommands() - if err != nil { - t.Errorf("invalid clipboard: %v", err) - } - if c.MaxTime != 45 { - t.Error("invalid default") - } - os.Setenv("LOCKBOX_CLIPMAX", "1") - c, err = NewCommands() - if err != nil { - t.Errorf("invalid clipboard: %v", err) - } - if c.MaxTime != 1 { - t.Error("invalid default") - } - os.Setenv("LOCKBOX_CLIPMAX", "-1") - _, err = NewCommands() - if err == nil || err.Error() != "clipboard max time must be greater than 0" { - t.Errorf("invalid max time error: %v", err) - } - os.Setenv("LOCKBOX_CLIPMAX", "$&(+") - _, err = NewCommands() - if err == nil || err.Error() != "strconv.Atoi: parsing \"$&(+\": invalid syntax" { - t.Errorf("invalid max time error: %v", err) - } -} - -func TestClipboardInstances(t *testing.T) { - os.Setenv("LOCKBOX_CLIPMAX", "") - for _, item := range []string{pbClipMode, xClipMode, waylandClipMode, wslMode} { - os.Setenv("LOCKBOX_CLIPMODE", item) - c, err := NewCommands() - if err != nil { - t.Errorf("invalid clipboard: %v", err) - } - if len(c.copying) == 0 || len(c.pasting) == 0 { - t.Error("invalid command retrieved") - } - } -} - -func TestArgs(t *testing.T) { - c := Commands{copying: []string{"cp"}, pasting: []string{"paste", "with", "args"}} - cmd, args := c.Args(true) - if cmd != "cp" || len(args) != 0 { - t.Error("invalid parse") - } - cmd, args = c.Args(false) - if cmd != "paste" || len(args) != 2 || args[0] != "with" || args[1] != "args" { - t.Error("invalid parse") - } -} diff --git a/internal/inputs/env.go b/internal/inputs/env.go @@ -20,6 +20,11 @@ func isYesNoEnv(defaultValue bool, env string) (bool, error) { return false, fmt.Errorf("invalid yes/no env value for %s", env) } +// IsNoClipEnabled indicates if clipboard mode is enabled. +func IsNoClipEnabled() (bool, error) { + return isYesNoEnv(false, "LOCKBOX_NOCLIP") +} + // IsNoColorEnabled indicates if the flag is set to disable color. func IsNoColorEnabled() (bool, error) { return isYesNoEnv(false, "LOCKBOX_NOCOLOR") diff --git a/internal/platform/clipboard.go b/internal/platform/clipboard.go @@ -0,0 +1,121 @@ +package platform + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + + "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/misc" +) + +const ( + maxTime = 45 +) + +type ( + // Clipboard represent system clipboard operations. + Clipboard struct { + copying []string + pasting []string + MaxTime int + } +) + +// NewClipboard will retrieve the commands to use for clipboard operations. +func NewClipboard() (Clipboard, error) { + noClip, err := inputs.IsNoClipEnabled() + if err != nil { + return Clipboard{}, err + } + if noClip { + return Clipboard{}, errors.New("clipboard is off") + } + sys, err := NewPlatform() + if err != nil { + return Clipboard{}, err + } + max := maxTime + useMax := os.Getenv("LOCKBOX_CLIPMAX") + if useMax != "" { + i, err := strconv.Atoi(useMax) + if err != nil { + return Clipboard{}, err + } + if i < 1 { + return Clipboard{}, errors.New("clipboard max time must be greater than 0") + } + max = i + } + var copying []string + var pasting []string + switch sys { + case MacOS: + copying = []string{"pbcopy"} + pasting = []string{"pbpaste"} + case LinuxX: + copying = []string{"xclip"} + pasting = []string{"xclip", "-o"} + case LinuxWayland: + copying = []string{"wl-copy"} + pasting = []string{"wl-paste"} + case WindowsLinux: + copying = []string{"clip.exe"} + pasting = []string{"powershell.exe", "-command", "Get-Clipboard"} + default: + return Clipboard{}, errors.New("clipboard is unavailable") + } + return Clipboard{copying: copying, pasting: pasting, MaxTime: max}, nil +} + +// CopyTo will copy to clipboard, if non-empty will clear later. +func (c Clipboard) CopyTo(value, executable string) { + cmd, args := c.Args(true) + pipeTo(cmd, value, true, args...) + if value != "" { + fmt.Printf("clipboard will clear in %d seconds\n", c.MaxTime) + pipeTo(filepath.Join(filepath.Dir(executable), "lb"), value, false, "clear") + } +} + +// Args returns clipboard args for execution. +func (c Clipboard) Args(copying bool) (string, []string) { + var using []string + if copying { + using = c.copying + } else { + using = c.pasting + } + var args []string + if len(using) > 1 { + args = using[1:] + } + return using[0], args +} + +func pipeTo(command, value string, wait bool, args ...string) { + cmd := exec.Command(command, args...) + stdin, err := cmd.StdinPipe() + if err != nil { + misc.Die("unable to get stdin pipe", err) + } + + go func() { + defer stdin.Close() + if _, err := stdin.Write([]byte(value)); err != nil { + fmt.Printf("failed writing to stdin: %v\n", err) + } + }() + var ran error + if wait { + ran = cmd.Run() + } else { + ran = cmd.Start() + } + if ran != nil { + misc.Die("failed to run command", ran) + } +} diff --git a/internal/platform/clipboard_test.go b/internal/platform/clipboard_test.go @@ -0,0 +1,73 @@ +package platform + +import ( + "os" + "testing" +) + +func TestNoClipboard(t *testing.T) { + os.Setenv("LOCKBOX_CLIPMAX", "") + os.Setenv("LOCKBOX_NOCLIP", "yes") + _, err := NewClipboard() + if err == nil || err.Error() != "clipboard is off" { + t.Errorf("invalid error: %v", err) + } +} + +func TestMaxTime(t *testing.T) { + os.Setenv("LOCKBOX_NOCLIP", "no") + os.Setenv("LOCKBOX_PLATFORM", string(LinuxWayland)) + os.Setenv("LOCKBOX_CLIPMAX", "") + c, err := NewClipboard() + if err != nil { + t.Errorf("invalid clipboard: %v", err) + } + if c.MaxTime != 45 { + t.Error("invalid default") + } + os.Setenv("LOCKBOX_CLIPMAX", "1") + c, err = NewClipboard() + if err != nil { + t.Errorf("invalid clipboard: %v", err) + } + if c.MaxTime != 1 { + t.Error("invalid default") + } + os.Setenv("LOCKBOX_CLIPMAX", "-1") + _, err = NewClipboard() + if err == nil || err.Error() != "clipboard max time must be greater than 0" { + t.Errorf("invalid max time error: %v", err) + } + os.Setenv("LOCKBOX_CLIPMAX", "$&(+") + _, err = NewClipboard() + if err == nil || err.Error() != "strconv.Atoi: parsing \"$&(+\": invalid syntax" { + t.Errorf("invalid max time error: %v", err) + } +} + +func TestClipboardInstances(t *testing.T) { + os.Setenv("LOCKBOX_NOCLIP", "no") + os.Setenv("LOCKBOX_CLIPMAX", "") + for _, item := range []System{MacOS, LinuxWayland, LinuxX, WindowsLinux} { + os.Setenv("LOCKBOX_PLATFORM", string(item)) + c, err := NewClipboard() + if err != nil { + t.Errorf("invalid clipboard: %v", err) + } + if len(c.copying) == 0 || len(c.pasting) == 0 { + t.Error("invalid command retrieved") + } + } +} + +func TestArgs(t *testing.T) { + c := Clipboard{copying: []string{"cp"}, pasting: []string{"paste", "with", "args"}} + cmd, args := c.Args(true) + if cmd != "cp" || len(args) != 0 { + t.Error("invalid parse") + } + cmd, args = c.Args(false) + if cmd != "paste" || len(args) != 2 || args[0] != "with" || args[1] != "args" { + t.Error("invalid parse") + } +} diff --git a/internal/platform/core.go b/internal/platform/core.go @@ -0,0 +1,67 @@ +package platform + +import ( + "errors" + "os" + "os/exec" + "strings" +) + +type ( + // System represents the platform lockbox is running on. + System string +) + +const ( + // MacOS based systems. + MacOS System = "macos" + // LinuxWayland running Wayland. + LinuxWayland System = "linux-wayland" + // LinuxX running X. + LinuxX System = "linux-x" + // WindowsLinux with WSL. + WindowsLinux System = "wsl" + // Unknown platform. + Unknown = "" +) + +// NewPlatform gets a new system platform. +func NewPlatform() (System, error) { + env := os.Getenv("LOCKBOX_PLATFORM") + if env != "" { + switch env { + case string(MacOS): + return MacOS, nil + case string(LinuxWayland): + return LinuxWayland, nil + case string(WindowsLinux): + return WindowsLinux, nil + case string(LinuxX): + return LinuxX, nil + default: + return Unknown, errors.New("unknown platform mode") + } + } + b, err := exec.Command("uname", "-a").Output() + if err != nil { + return Unknown, err + } + raw := strings.TrimSpace(string(b)) + parts := strings.Split(raw, " ") + switch parts[0] { + case "Darwin": + return MacOS, nil + case "Linux": + if strings.Contains(raw, "microsoft-standard-WSL2") { + return WindowsLinux, nil + } + if strings.TrimSpace(os.Getenv("WAYLAND_DISPLAY")) == "" { + if strings.TrimSpace(os.Getenv("DISPLAY")) == "" { + return Unknown, errors.New("unable to detect linux clipboard mode") + } + return LinuxX, nil + } + return LinuxWayland, nil + } + return Unknown, errors.New("unable to detect clipboard mode") +} diff --git a/internal/platform/core_test.go b/internal/platform/core_test.go @@ -0,0 +1,27 @@ +package platform + +import ( + "os" + "testing" +) + +func TestNewPlatform(t *testing.T) { + for _, item := range []System{MacOS, LinuxWayland, LinuxX, WindowsLinux} { + os.Setenv("LOCKBOX_PLATFORM", string(item)) + s, err := NewPlatform() + if err != nil { + t.Errorf("invalid clipboard: %v", err) + } + if s != item { + t.Error("mismatch on input and resulting detection") + } + } +} + +func TestNewPlatformUnknown(t *testing.T) { + os.Setenv("LOCKBOX_PLATFORM", "afleaj") + _, err := NewPlatform() + if err == nil || err.Error() != "unknown platform mode" { + t.Errorf("error expected for platform: %v", err) + } +}