From 69a008951e47a1f06daaa0a23cc0b5445c270462 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 13 Feb 2023 19:35:47 -0800 Subject: [PATCH] validate server connection before dismissing login dialogs --- backend/servermanager.go | 44 ++++++++++++++++++++---- ui/controller/controller.go | 57 +++++++++++++++++++++---------- ui/dialogs/addeditserverdialog.go | 23 +++++++++++-- ui/mainwindow.go | 2 +- 4 files changed, 97 insertions(+), 29 deletions(-) diff --git a/backend/servermanager.go b/backend/servermanager.go index a5568b3..12d7b91 100644 --- a/backend/servermanager.go +++ b/backend/servermanager.go @@ -1,7 +1,9 @@ package backend import ( + "errors" "net/http" + "time" "github.com/dweymouth/go-subsonic/subsonic" "github.com/google/uuid" @@ -16,18 +18,15 @@ type ServerManager struct { onLogout []func() } +var ErrUnreachable = errors.New("server is unreachable") + func NewServerManager() *ServerManager { return &ServerManager{} } func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) error { - cli := &subsonic.Client{ - Client: &http.Client{}, - BaseUrl: conf.Hostname, - User: conf.Username, - ClientName: "supersonic", - } - if err := cli.Authenticate(password); err != nil { + cli, err := s.testConnectionAndCreateClient(conf.Hostname, conf.Username, password) + if err != nil { return err } s.Server = cli @@ -38,6 +37,37 @@ func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) err return nil } +func (s *ServerManager) TestConnectionAndAuth(hostname, username, password string) error { + err := ErrUnreachable + done := make(chan bool) + go func() { + _, err = s.testConnectionAndCreateClient(hostname, username, password) + close(done) + }() + select { + case <-time.After(200 * time.Millisecond): + return err + case <-done: + return err + } +} + +func (s *ServerManager) testConnectionAndCreateClient(hostname, username, password string) (*subsonic.Client, error) { + cli := &subsonic.Client{ + Client: &http.Client{}, + BaseUrl: hostname, + User: username, + ClientName: "supersonic", + } + if !cli.Ping() { + return nil, ErrUnreachable + } + if err := cli.Authenticate(password); err != nil { + return nil, err + } + return cli, nil +} + func (s *ServerManager) Logout() { if s.Server != nil { keyring.Delete(AppName, s.ServerID.String()) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index b9fc176..f545b64 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -42,13 +42,16 @@ func (m Controller) PromptForFirstServer() { d := dialogs.NewAddEditServerDialog("Connect to Server", nil) pop := widget.NewModalPopUp(d, m.MainWindow.Canvas()) d.OnSubmit = func() { - pop.Hide() - server := m.App.Config.AddServer(d.Nickname, d.Host, d.Username) - if err := m.App.ServerManager.SetServerPassword(server, d.Password); err != nil { - log.Printf("error setting keyring credentials: %v", err) - // TODO: handle? + if m.testConnectionAndUpdateDialogError(d) { + // connection is good + pop.Hide() + server := m.App.Config.AddServer(d.Nickname, d.Host, d.Username) + if err := m.App.ServerManager.SetServerPassword(server, d.Password); err != nil { + log.Printf("error setting keyring credentials: %v", err) + // TODO: handle? + } + m.DoConnectToServerWorkflow(server) } - m.DoConnectToServerWorkflow(server) } pop.Show() } @@ -88,13 +91,13 @@ func (c Controller) DoConnectToServerWorkflow(server *backend.ServerConfig) { pass, err := c.App.ServerManager.GetServerPassword(server) if err != nil { log.Printf("error getting password from keyring: %v", err) - c.PromptForLogin() + c.PromptForLoginAndConnect() } else { c.tryConnectToServer(server, pass) } } -func (m Controller) PromptForLogin() { +func (m Controller) PromptForLoginAndConnect() { // TODO: this will need to be rewritten a bit when we support multi servers // need to make sure the intended server is first in the list passed to NewLoginDialog d := dialogs.NewLoginDialog(m.App.Config.Servers) @@ -108,28 +111,46 @@ func (m Controller) PromptForLogin() { editD := dialogs.NewAddEditServerDialog("Edit server", server) editPop := widget.NewModalPopUp(editD, m.MainWindow.Canvas()) editD.OnSubmit = func() { - editPop.Hide() - server.Hostname = editD.Host - server.Nickname = editD.Nickname - server.Username = editD.Username - m.trySetPasswordAndConnectToServer(server, editD.Password) + if m.testConnectionAndUpdateDialogError(editD) { + // connection is good + editPop.Hide() + server.Hostname = editD.Host + server.Nickname = editD.Nickname + server.Username = editD.Username + m.trySetPasswordAndConnectToServer(server, editD.Password) + } } editPop.Show() } pop.Show() } -func (c Controller) trySetPasswordAndConnectToServer(server *backend.ServerConfig, password string) { +func (c Controller) trySetPasswordAndConnectToServer(server *backend.ServerConfig, password string) error { if err := c.App.ServerManager.SetServerPassword(server, password); err != nil { log.Printf("error setting keyring credentials: %v", err) - // TODO: handle? + // TODO: how best to handle this unexpected codepath + // fall back to prompting for password on each run of the app? + return err } - c.tryConnectToServer(server, password) + return c.tryConnectToServer(server, password) } -func (c Controller) tryConnectToServer(server *backend.ServerConfig, password string) { +func (c Controller) tryConnectToServer(server *backend.ServerConfig, password string) error { if err := c.App.ServerManager.ConnectToServer(server, password); err != nil { log.Printf("error connecting to server: %v", err) - // TODO: surface error to user + return err } + return nil +} + +func (c Controller) testConnectionAndUpdateDialogError(dlg *dialogs.AddEditServerDialog) bool { + err := c.App.ServerManager.TestConnectionAndAuth(dlg.Host, dlg.Username, dlg.Password) + if err == backend.ErrUnreachable { + dlg.SetErrorText("Could not reach server (wrong hostname?)") + return false + } else if err != nil { + dlg.SetErrorText("Authentication failed (wrong username/password)") + return false + } + return true } diff --git a/ui/dialogs/addeditserverdialog.go b/ui/dialogs/addeditserverdialog.go index a6e1bc5..4f81fa9 100644 --- a/ui/dialogs/addeditserverdialog.go +++ b/ui/dialogs/addeditserverdialog.go @@ -7,6 +7,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -19,7 +20,8 @@ type AddEditServerDialog struct { Password string OnSubmit func() - container *fyne.Container + errPromptText *widget.RichText + container *fyne.Container } var _ fyne.Widget = (*AddEditServerDialog)(nil) @@ -47,6 +49,9 @@ func NewAddEditServerDialog(title string, prefillServer *backend.ServerConfig) * a.OnSubmit() } }) + a.errPromptText = widget.NewRichTextWithText("") + a.errPromptText.Segments[0].(*widget.TextSegment).Style.ColorName = theme.ColorNameError + a.errPromptText.Hidden = true a.container = container.NewVBox( container.NewHBox(layout.NewSpacer(), titleLabel, layout.NewSpacer()), @@ -62,14 +67,26 @@ func NewAddEditServerDialog(title string, prefillServer *backend.ServerConfig) * ), widget.NewSeparator(), container.NewHBox( + a.errPromptText, layout.NewSpacer(), - submit)) + submit), + ) return a } +func (a *AddEditServerDialog) SetErrorText(text string) { + a.errPromptText.Segments[0].(*widget.TextSegment).Text = text + if text != "" { + a.errPromptText.Show() + } else { + a.errPromptText.Hide() + } + a.errPromptText.Refresh() +} + func (a *AddEditServerDialog) MinSize() fyne.Size { a.ExtendBaseWidget(a) - return fyne.NewSize(300, a.container.MinSize().Height) + return fyne.NewSize(450, a.container.MinSize().Height) } func (a *AddEditServerDialog) CreateRenderer() fyne.WidgetRenderer { diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 123ffa0..507faf9 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -69,7 +69,7 @@ func NewMainWindow(fyneApp fyne.App, appName string, app *backend.App, size fyne m.BrowsingPane.DisableNavigationButtons() m.BrowsingPane.SetPage(nil) m.BrowsingPane.ClearHistory() - m.Controller.PromptForLogin() + m.Controller.PromptForLoginAndConnect() }) m.BrowsingPane.AddSettingsMenuItem("Log Out", app.ServerManager.Logout) m.addNavigationButtons()