196 lines
5.3 KiB
Go
196 lines
5.3 KiB
Go
package appui
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"gioui.org/app"
|
|
"gioui.org/op"
|
|
"gioui.org/unit"
|
|
"gioui.org/x/explorer"
|
|
"git.julianfamily.org/keepassgo/internal/api"
|
|
"git.julianfamily.org/keepassgo/internal/apiapproval"
|
|
"git.julianfamily.org/keepassgo/internal/apitokens"
|
|
"git.julianfamily.org/keepassgo/internal/appui/platform"
|
|
"git.julianfamily.org/keepassgo/internal/grpcaddr"
|
|
"git.julianfamily.org/keepassgo/internal/passwords"
|
|
"git.julianfamily.org/keepassgo/internal/session"
|
|
"git.julianfamily.org/keepassgo/internal/vault"
|
|
)
|
|
|
|
func Main() {
|
|
mode := flag.String("mode", "", "window mode: desktop or phone")
|
|
stateDir := flag.String("state-dir", "", "directory for KeePassGO state such as recent-vault history and default save targets")
|
|
grpcAddr := flag.String("grpc-addr", "", "address for the local gRPC API listener; use 'off' to disable")
|
|
flag.Parse()
|
|
|
|
resolvedMode := resolveFlagOrEnv(*mode, "KEEPASSGO_MODE", defaultModeForRuntime(runtime.GOOS))
|
|
resolvedStateDir := resolveFlagOrEnv(*stateDir, "KEEPASSGO_STATE_DIR", "")
|
|
resolvedGRPCAddr := resolveFlagOrEnv(*grpcAddr, "KEEPASSGO_GRPC_ADDR", defaultGRPCAddr(runtime.GOOS))
|
|
|
|
width := unit.Dp(1180)
|
|
height := unit.Dp(760)
|
|
if strings.EqualFold(resolvedMode, "phone") {
|
|
width = unit.Dp(412)
|
|
height = unit.Dp(915)
|
|
}
|
|
|
|
go func() {
|
|
w := new(app.Window)
|
|
options := []app.Option{app.Title(productName)}
|
|
if shouldUsePreviewWindowSize(resolvedMode, runtime.GOOS) {
|
|
options = append(options, app.Size(width, height))
|
|
}
|
|
w.Option(options...)
|
|
if err := run(w, strings.ToLower(resolvedMode), defaultStatePaths(resolvedStateDir), resolvedGRPCAddr); err != nil {
|
|
panic(err)
|
|
}
|
|
if !strings.EqualFold(runtime.GOOS, "android") {
|
|
os.Exit(0)
|
|
}
|
|
}()
|
|
app.Main()
|
|
}
|
|
|
|
func defaultGRPCAddr(goos string) string {
|
|
return grpcaddr.Default(goos)
|
|
}
|
|
|
|
func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error {
|
|
var ops op.Ops
|
|
manager := &session.Manager{}
|
|
ui := newUIWithSession(mode, manager, paths)
|
|
ui.fileExplorer = explorer.NewExplorer(w)
|
|
ui.invalidate = w.Invalidate
|
|
ui.clipboardWriter = platform.NewClipboardWriter(runtime.GOOS, w.Invalidate)
|
|
host, err := api.StartHost(grpcAddr, manager, passwords.DefaultProfiles(), ui.clipboardWriter, func() bool { return ui.state.Dirty })
|
|
if err != nil {
|
|
ui.state.ErrorMessage = fmt.Sprintf("start gRPC API: %v", err)
|
|
} else if host != nil {
|
|
ui.apiHost = host
|
|
ui.auditLog = host.Server().AuditLog()
|
|
ui.state.AuditLog = ui.auditLog
|
|
ui.grpcAddress = host.Address()
|
|
ui.state.Approvals = &uiApprovalManager{server: host.Server()}
|
|
host.Server().SetChangeNotifier(func() {
|
|
ui.state.Dirty = true
|
|
ui.invalidate()
|
|
})
|
|
host.Server().ApprovalBroker().SetChangeNotifier(ui.invalidate)
|
|
defer func() { _ = host.Stop() }()
|
|
}
|
|
for {
|
|
e := w.Event()
|
|
ui.fileExplorer.ListenEvents(e)
|
|
switch e := e.(type) {
|
|
case app.DestroyEvent:
|
|
return e.Err
|
|
case app.FrameEvent:
|
|
gtx := app.NewContext(&ops, e)
|
|
ui.processBackgroundActions()
|
|
ui.layout(gtx)
|
|
platform.ProcessClipboardWrites(gtx, ui.clipboardWriter)
|
|
e.Frame(gtx.Ops)
|
|
}
|
|
}
|
|
}
|
|
|
|
type uiApprovalManager struct {
|
|
server *api.Server
|
|
}
|
|
|
|
func (m *uiApprovalManager) Pending() []apiapproval.Request {
|
|
if m == nil || m.server == nil {
|
|
return nil
|
|
}
|
|
return m.server.ApprovalBroker().Pending()
|
|
}
|
|
|
|
func (m *uiApprovalManager) Resolve(id string, outcome apiapproval.Outcome) (apiapproval.Request, *apitokens.PolicyRule, error) {
|
|
if m == nil || m.server == nil {
|
|
return apiapproval.Request{}, nil, fmt.Errorf("approval manager is not configured")
|
|
}
|
|
return m.server.ResolveApproval(id, outcome)
|
|
}
|
|
|
|
type uiSession struct {
|
|
model vault.Model
|
|
locked bool
|
|
}
|
|
|
|
func (s *uiSession) HasVault() bool {
|
|
return len(s.model.Entries) > 0 || len(s.model.Templates) > 0 || len(s.model.RecycleBin) > 0 || len(s.model.Groups) > 0 || s.locked
|
|
}
|
|
|
|
func (s *uiSession) IsLocked() bool {
|
|
return s.locked
|
|
}
|
|
|
|
func (s *uiSession) IsRemote() bool {
|
|
return false
|
|
}
|
|
|
|
func (s *uiSession) Current() (vault.Model, error) {
|
|
if s.locked {
|
|
return vault.Model{}, session.ErrLocked
|
|
}
|
|
return s.model, nil
|
|
}
|
|
|
|
func (s *uiSession) Replace(model vault.Model) {
|
|
s.model = model
|
|
}
|
|
|
|
func (s *uiSession) Lock() error {
|
|
s.locked = true
|
|
return nil
|
|
}
|
|
|
|
func (s *uiSession) Unlock(vault.MasterKey) error {
|
|
if !s.locked {
|
|
return nil
|
|
}
|
|
s.locked = false
|
|
return nil
|
|
}
|
|
|
|
func pickExistingFile() (string, error) {
|
|
if path, err := runFilePicker("kdialog", "--getopenfilename", "--title", "Choose KeePass file"); err == nil {
|
|
return path, nil
|
|
}
|
|
if path, err := runFilePicker("zenity", "--file-selection", "--title=Choose KeePass file"); err == nil {
|
|
return path, nil
|
|
}
|
|
return "", fmt.Errorf("no supported file picker found; install kdialog or zenity")
|
|
}
|
|
|
|
func runFilePicker(name string, args ...string) (string, error) {
|
|
if _, err := exec.LookPath(name); err != nil {
|
|
return "", err
|
|
}
|
|
cmd := exec.Command(name, args...)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return parsePickedFilePath(output)
|
|
}
|
|
|
|
func parsePickedFilePath(output []byte) (string, error) {
|
|
lines := strings.Split(strings.ReplaceAll(string(output), "\r\n", "\n"), "\n")
|
|
for i := len(lines) - 1; i >= 0; i-- {
|
|
line := strings.TrimSpace(lines[i])
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "/") || strings.HasPrefix(line, "~/") {
|
|
return line, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("file picker did not return a path")
|
|
}
|