package api import ( "errors" "fmt" "net" "strings" "sync" "git.julianfamily.org/keepassgo/clipboard" "git.julianfamily.org/keepassgo/passwords" keepassgov1 "git.julianfamily.org/keepassgo/proto/keepassgo/v1" "git.julianfamily.org/keepassgo/session" "git.julianfamily.org/keepassgo/vault" "google.golang.org/grpc" ) type DirtyProvider func() bool type Host struct { server *Server grpcServer *grpc.Server listener net.Listener lifecycle lifecycleBackend dirty DirtyProvider mu sync.Mutex lastModel vault.Model started bool listenAddr string } func StartHost(addr string, lifecycle lifecycleBackend, profiles map[string]passwords.Profile, clipboardWriter clipboard.Writer, dirty DirtyProvider) (*Host, error) { addr = strings.TrimSpace(addr) if addr == "" || strings.EqualFold(addr, "off") { return nil, nil } listener, err := net.Listen("tcp", addr) if err != nil { return nil, fmt.Errorf("listen gRPC host %s: %w", addr, err) } service := NewServerWithLifecycle(vault.Model{}, profiles, clipboardWriter, lifecycle) server := grpc.NewServer(grpc.UnaryInterceptor(AuthInterceptor(service))) keepassgov1.RegisterVaultServiceServer(server, service) host := &Host{ server: service, grpcServer: server, listener: listener, lifecycle: lifecycle, dirty: dirty, listenAddr: listener.Addr().String(), started: true, } if err := host.SyncFromLifecycle(); err != nil && !errors.Is(err, session.ErrLocked) { _ = listener.Close() server.Stop() return nil, err } go func() { _ = server.Serve(listener) }() return host, nil } func (h *Host) Address() string { if h == nil { return "" } return h.listenAddr } func (h *Host) Server() *Server { if h == nil { return nil } return h.server } func (h *Host) Stop() error { if h == nil { return nil } h.mu.Lock() defer h.mu.Unlock() if !h.started { return nil } h.started = false h.grpcServer.Stop() return h.listener.Close() } func (h *Host) SyncFromLifecycle() error { if h == nil || h.lifecycle == nil || h.server == nil { return nil } h.mu.Lock() defer h.mu.Unlock() model, err := h.lifecycle.Current() locked := false switch { case err == nil: h.lastModel = model case errors.Is(err, session.ErrLocked): locked = true default: return err } dirty := false if h.dirty != nil { dirty = h.dirty() } h.server.SetSessionState(h.lastModel, locked, dirty) return nil }