diff --git a/src/client/nexus-exam/nexus-exam.go b/src/client/nexus-exam/nexus-exam.go index 4da49b36ed7315ea1f585dd4be9a618c0bdb6f67..846d19a97b6a7a7c15a03141fc0081cbe7e63d9c 100644 --- a/src/client/nexus-exam/nexus-exam.go +++ b/src/client/nexus-exam/nexus-exam.go @@ -2,6 +2,7 @@ package main import ( "os" + "time" "bytes" "errors" "strings" @@ -13,14 +14,12 @@ import ( e "nexus-client/exec" u "nexus-client/utils" g "nexus-client/globals" - // "nexus-client/cmdVersion" "nexus-client/defaults" "nexus-client/cmdLogin" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/container" "github.com/go-resty/resty/v2" "github.com/go-playground/validator/v10" @@ -37,6 +36,7 @@ var ( //go:embed nexus_exam_pwd.val nexus_exam_pwd string + token string certPath string exitFn func() ) @@ -48,35 +48,32 @@ func exit(code int) { os.Exit(code) } -func errorDialog(parent fyne.Window, msg string) { - dialog.NewInformation("Connection error", msg, parent).Show() -} - -func attachVM(parent fyne.Window, token, hostname, cert, pwd string) (int, error) { +func attachVM(parent fyne.Window, hostname, cert, pwd string) { client := g.GetInstance().Client host := g.GetInstance().Host - client.SetHeader("Content-Type", "application/json") - client.SetHeader("Authorization", "Bearer "+token) - p := ¶ms.VMAttachCreds{ Pwd: pwd } resp, err := client.R().SetBody(p).Post(host+"/vms/spicecreds") if err != nil { - errorDialog(parent, err.Error()) + errorPopup(parent, "Failed attaching to VM (code 4)") + return } if resp.IsSuccess() { var creds vm.VMSpiceCredentialsSerialized if err := json.Unmarshal(resp.Body(), &creds); err != nil { - errorDialog(parent, err.Error()) + errorPopup(parent, "Failed attaching to VM (code 5)") + return } if err := validator.New(validator.WithRequiredStructEnabled()).Struct(creds); err != nil { - errorDialog(parent, err.Error()) + errorPopup(parent, "Failed attaching to VM (code 6)") + return } go func(creds vm.VMSpiceCredentialsSerialized) { _, err := e.RunRemoteViewer(hostname, cert, creds.Name, creds.SpicePort, creds.SpicePwd, true) if err != nil { - errorDialog(parent, err.Error()) + errorPopup(parent, "Failed attaching to VM (code 7)") + return } } (creds) } else { @@ -85,15 +82,16 @@ func attachVM(parent fyne.Window, token, hostname, cert, pwd string) (int, error } var m msg if err := json.Unmarshal(resp.Body(), &m); err != nil { - errorDialog(parent, err.Error()) + errorPopup(parent, "Failed attaching to VM (code 8)") + return } - errorDialog(parent, m.Message) - // errorDialog(parent, "Error: "+resp.Status()+": "+resp.String()) + errorPopup(parent, m.Message) + // errorPopup(parent, "Error: "+resp.Status()+": "+resp.String()) + return } - return 0, nil } -func abortWindow(labelMsg string) { +func abortWindow(msg string) { a := app.New() a.Settings().SetTheme(theme.LightTheme()) win := a.NewWindow(windowTitle) @@ -101,8 +99,8 @@ func abortWindow(labelMsg string) { win.Close() exit(1) }) - label := widget.NewLabel("ERROR: "+labelMsg) - button := widget.NewButton("OK", func() { + label := widget.NewLabel("FATAL: "+msg) + button := widget.NewButton("Quit", func() { win.Close() exit(1) }) @@ -111,6 +109,16 @@ func abortWindow(labelMsg string) { win.ShowAndRun() } +func errorPopup(win fyne.Window, msg string) { + var modal *widget.PopUp + modal = widget.NewModalPopUp( + container.NewVBox( + widget.NewLabel("Error: "+msg), + widget.NewButton("Close", func() { modal.Hide() })), + win.Canvas()) + modal.Show() +} + func hypervisorCheck() { cmd := exec.Command("systemd-detect-virt") var out bytes.Buffer @@ -122,6 +130,35 @@ func hypervisorCheck() { } } +// Recurrently obtains a new JWT token so that the user session doesn't expire. +func refreshToken(parent fyne.Window) { + client := g.GetInstance().Client + host := g.GetInstance().Host + + for { + resp, err := client.R().Get(host+"/token/refresh") + if err != nil { + errorPopup(parent, "Failed refreshing token (code 1)") + } else { + if resp.IsSuccess() { + type Response struct { + Token string + } + var response Response + err = json.Unmarshal(resp.Body(), &response) + if err != nil { + errorPopup(parent, "Failed refreshing token (code 2)") + } + token = response.Token + } else { + // errorPopup(parent, resp.Status()+": "+resp.String()) + errorPopup(parent, "Failed refreshing token (code 3)") + } + } + time.Sleep(4*time.Hour) + } +} + func run() int { hypervisorCheck() @@ -166,17 +203,22 @@ func run() int { g.Init(hostname, host, certPath, client) + client.SetTimeout(10*time.Second) + // Checks the client version is compatible with the server's API. // if !cmdVersion.CheckServerCompatibility("nexus-exam") { // abortWindow("client version is incompatible with server!") // } // Logins and obtains a JWT token. - token, err := cmdLogin.GetToken(nexus_exam_user, nexus_exam_pwd) + token, err = cmdLogin.GetToken(nexus_exam_user, nexus_exam_pwd) if err != nil { - abortWindow(err.Error()) + abortWindow("Failed obtaining token (network issue?)") } + client.SetHeader("Content-Type", "application/json") + client.SetHeader("Authorization", "Bearer "+token) + app := app.New() app.Settings().SetTheme(theme.LightTheme()) win := app.NewWindow(windowTitle) @@ -198,13 +240,16 @@ func run() int { {Text: "Password", Widget: pwdEntry}, }, OnSubmit: func() { - attachVM(win, token, hostname, certPath, pwdEntry.Text) + attachVM(win, hostname, certPath, pwdEntry.Text) }, SubmitText: "Connect", } win.SetContent(container.NewPadded(container.NewVBox(label, form))) win.Resize(fyne.NewSize(600,200)) + + go refreshToken(win) + win.ShowAndRun() return 0 } diff --git a/src/server/router/auth.go b/src/server/router/auth.go index 465ac426170b206f00f575a04ebed226718449af..c142fd2e404ca81597ea092448269156aae8ea2b 100644 --- a/src/server/router/auth.go +++ b/src/server/router/auth.go @@ -50,7 +50,7 @@ func NewAuth(u *users.Users) (*auth, error) { SigningKey: []byte(jwtSecretKey), Claims: &customClaims{}, ErrorHandlerWithContext: func(err error, c echo.Context) error { - return echo.NewHTTPError(http.StatusUnauthorized, "access denied") + return echo.NewHTTPError(http.StatusUnauthorized, "token expired: access denied") }, }, }, nil