From 078123fbbf5935e4282593d20549a92090148821 Mon Sep 17 00:00:00 2001 From: Florent Gluck <florent.gluck@hesge.ch> Date: Mon, 24 Mar 2025 01:33:30 +0100 Subject: [PATCH] ongoing work on clean solution for CA certificate --- client/nexus-exam/nexus-exam.go | 48 ++++++++++++++----- client/nexush/nexush.go | 29 ++++++++---- libclient/client.go | 29 +++++++----- libclient/remote-viewer.go | 28 ++++++----- libclient/vm.go | 83 ++++++++++++--------------------- 5 files changed, 123 insertions(+), 94 deletions(-) diff --git a/client/nexus-exam/nexus-exam.go b/client/nexus-exam/nexus-exam.go index 8187cb73..62283c75 100644 --- a/client/nexus-exam/nexus-exam.go +++ b/client/nexus-exam/nexus-exam.go @@ -33,16 +33,23 @@ var ( //go:embed nexus_exam_pwd.val nexus_exam_pwd string - client *nc.NexusClient - + client *nc.NexusClient + exitFn func() isAuthenticated bool isServerUp bool statusLabel *widget.Label credentialsInputBox *widget.Form ) -func attachVM(parent fyne.Window, pwd string) { - err := client.VMAttachFromPwd(pwd, true) +func exit(code int) { + if exitFn != nil { + exitFn() + } + os.Exit(code) +} + +func attachVM(parent fyne.Window, certificateFile string, pwd string) { + err := client.VMAttachFromPwd(pwd, certificateFile, true) if err != nil { errorPopup(parent, "Failed attaching to VM: "+err.Error()) return @@ -55,12 +62,12 @@ func abortWindow(msg string) { win := a.NewWindow(windowTitle) win.SetOnClosed(func() { win.Close() - os.Exit(1) + exit(1) }) - label := widget.NewLabel("FATAL: " + msg) + label := widget.NewLabel("FATAL ERROR: " + msg) button := widget.NewButton("Quit", func() { win.Close() - os.Exit(1) + exit(1) }) content := container.NewPadded(container.NewVBox(label, button)) win.SetContent(content) @@ -151,8 +158,25 @@ func checkServerUp(parent fyne.Window) { func run() int { hypervisorCheck() - client = nc.New(defaults.NexusServer) - client.SetTimeout(4 * time.Second) + var err error + client, err = nc.New(defaults.NexusServer) + if err != nil { + abortWindow(err.Error()) + } + + certFile, err := client.GetVMAttachCertificate() + if err != nil { + abortWindow(err.Error()) + } + exitFn = func() { os.Remove(certFile) } + + // This thread acts as a signal handler for SIGINT or SIGTERM. + // When one of these signals is received, the temporary certificate file is deleted. + // Without this "handler", the temporary certificate file wouldn't be deleted. + go func() { + u.WaitForSignals() + exit(1) + }() isServerUp = false isAuthenticated = false @@ -180,7 +204,7 @@ func run() int { return nil } - statusLabel = widget.NewLabel("") + statusLabel = widget.NewLabel("\n\n\n") statusLabel.Show() credentialsInputBox = &widget.Form{ @@ -188,7 +212,7 @@ func run() int { {Text: "Password", Widget: pwdEntry}, }, OnSubmit: func() { - attachVM(win, pwdEntry.Text) + attachVM(win, certFile, pwdEntry.Text) }, SubmitText: "Connect", } @@ -205,5 +229,5 @@ func run() int { } func main() { - os.Exit(run()) + exit(run()) } diff --git a/client/nexush/nexush.go b/client/nexush/nexush.go index e5674504..4b28602b 100644 --- a/client/nexush/nexush.go +++ b/client/nexush/nexush.go @@ -102,14 +102,11 @@ func run() int { savedTermState, _ = term.GetState(int(os.Stdin.Fd())) - // This thread acts as a signal handler for SIGINT or SIGTERM. - go func() { - u.WaitForSignals() - restoreTerm() - os.Exit(1) - }() - - nc := nc.New(serverEnvVar) + nc, err := nc.New(serverEnvVar) + if err != nil { + u.PrintlnErr("Error: " + err.Error()) + return 1 + } // Checks the client version is compatible with the server's API. if !cmdVersion.CheckServerCompatibility(nc, appname) { @@ -133,6 +130,22 @@ func run() int { return 1 } + certFile, err := nc.GetVMAttachCertificate() + if err != nil { + u.PrintlnErr("Error: " + err.Error()) + return 1 + } + + // This thread acts as a signal handler for SIGINT or SIGTERM. + // When one of these signals is received, the temporary certificate file is deleted. + // Without this "handler", the temporary certificate file wouldn't be deleted. + go func() { + u.WaitForSignals() + os.Remove(certFile) + restoreTerm() + os.Exit(1) + }() + shell(nc) return 0 } diff --git a/libclient/client.go b/libclient/client.go index 956e3ed6..8434649f 100644 --- a/libclient/client.go +++ b/libclient/client.go @@ -9,30 +9,37 @@ import ( ) type NexusClient struct { - client *resty.Client + client *resty.Client + hostname string // host, e.g. "127.0.0.1" or "my.domain.net" } -// host has the following format: -// ip:port, e.g. 127.0.0.1:1077 -func New(host string) *NexusClient { - nc := &NexusClient{resty.New()} +// The host argument has the following format: +// "ip:port" or "domain:port", e.g. "127.0.0.1:1077" or "my.domain.net:80" +func New(host string) (*NexusClient, error) { + nc := &NexusClient{} + nc.client = resty.New() // To disable the check of the CA signed certificate nc.client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) nc.client.SetBaseURL("https://" + host) nc.client.SetAllowGetMethodPayload(true) + // Change the default timeout to 10 seconds + nc.client.SetTimeout(10 * time.Second) - return nc -} - -func (nc *NexusClient) GetBaseURL() (string, error) { url, err := url.Parse(nc.client.BaseURL) if err != nil { - return "", err + return nil, err } - return url.Hostname(), nil + nc.hostname = url.Hostname() + return nc, nil +} + +// Returns the hostname, e.g. "127.0.0.1" or "my.domain.net". +func (nc *NexusClient) GetHostname() string { + return nc.hostname } +// Change the default timeout. func (nc *NexusClient) SetTimeout(timeout time.Duration) { nc.client.SetTimeout(timeout) } diff --git a/libclient/remote-viewer.go b/libclient/remote-viewer.go index 9f6b9200..f69239e3 100644 --- a/libclient/remote-viewer.go +++ b/libclient/remote-viewer.go @@ -2,9 +2,9 @@ package nexusclient import ( "errors" + "fmt" "os/exec" "runtime" - "strconv" "strings" ) @@ -45,20 +45,26 @@ func checkRemoteViewer() error { // name is the VM's name. // port is the port the VM spice server is listening to. // pwd is the spice password to connect to the running VM. -func runRemoteViewer(hostname string, cert string, name string, port int, pwd string, fullscreen bool) ([]byte, error) { - portStr := strconv.Itoa(port) - spice := "spice://" + hostname + "?tls-port=" + portStr + "&password=" + pwd - spiceCert := "--spice-ca-file=" + cert - spiceSecure := "--spice-secure-channels=all" +func runRemoteViewer(hostname string, cert string, name string, port int, pwd string, fullscreen bool) error { + // portStr := strconv.Itoa(port) + // spice := "spice://" + hostname + "?tls-port=" + portStr + "&password=" + pwd + // spiceCert := "--spice-ca-file=" + cert + // spiceSecure := "--spice-secure-channels=all" // To exit fullscreen mode, either: // - move the mouse to the middle of the top of the screen. // - use ctrl+F12 as specified below // To activate kiosk mode, add these options: "-k --kiosk-quit=on-disconnect" var c *exec.Cmd - if fullscreen { - c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12", "-f") - } else { - c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12") + // if fullscreen { + // c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12", "-f") + // } else { + // c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12") + // } + c = exec.Command("remote-viewer", "spice://crap") + stdoutStderr, err := c.CombinedOutput() + if err != nil { + return errors.New("err: remote-viewer failed: " + string(stdoutStderr[:])) } - return c.CombinedOutput() + fmt.Println("err: remote-viewer failed: " + string(stdoutStderr[:])) + return nil } diff --git a/libclient/vm.go b/libclient/vm.go index 55124742..5db4c4ce 100644 --- a/libclient/vm.go +++ b/libclient/vm.go @@ -182,35 +182,24 @@ func (nc *NexusClient) VMStop(vmID string) error { } } -func (nc *NexusClient) VMAttachFromID(vmID string, fullscreen bool) error { +func (nc *NexusClient) VMAttachFromID(vmID string, certificateFile string, fullscreen bool) error { creds, err := nc.vmIdToAttachCreds(vmID) if err != nil { return err } - err = nc.VMAttachFromCreds(*creds, fullscreen) + err = nc.VMAttachFromCreds(*creds, certificateFile, fullscreen) if err != nil { return err } return nil } -func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, fullscreen bool) error { +func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, certificateFile string, fullscreen bool) error { if err := checkRemoteViewer(); err != nil { return err } - hostname, err := nc.GetBaseURL() - if err != nil { - return err - } - - certFile, err := nc.getSpiceCertificate() - if err != nil { - return err - } - defer os.Remove(certFile) - p := params.VMAttachCreds{Pwd: v.Pwd} creds, err := nc.vmGetSpiceCreds(v.ID.String(), p) if err != nil { @@ -218,7 +207,7 @@ func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, ful } // First return value (ignored) is of type: stdoutStderr - _, err = runRemoteViewer(hostname, certFile, creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen) + err = runRemoteViewer(nc.hostname, certificateFile, creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen) if err != nil { return err } @@ -226,28 +215,17 @@ func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, ful return nil } -func (nc *NexusClient) VMAttachFromPwd(pwd string, fullscreen bool) error { +func (nc *NexusClient) VMAttachFromPwd(pwd string, certificateFile string, fullscreen bool) error { if err := checkRemoteViewer(); err != nil { return err } - hostname, err := nc.GetBaseURL() - if err != nil { - return err - } - - certFile, err := nc.getSpiceCertificate() - if err != nil { - return err - } - defer os.Remove(certFile) - p := params.VMAttachCreds{Pwd: pwd} creds, err := nc.vmGetAnySpiceCreds(p) if err != nil { return err } else { - _, err := runRemoteViewer(hostname, certFile, creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen) + err := runRemoteViewer(nc.hostname, certificateFile, creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen) if err != nil { return err } @@ -256,6 +234,31 @@ func (nc *NexusClient) VMAttachFromPwd(pwd string, fullscreen bool) error { return nil } +// Obtain the certificate required by the various VMAttachXXX methods. +// This public CA certificate is required by Spice remote viewers. +// The certificate file is saved in the OS' temporary directory. +// Its name is randomly generated and garanteed to be unique. +// It is the caller's responsability to delete it before exiting. +func (nc *NexusClient) GetVMAttachCertificate() (string, error) { + uniqPostfix, err := uuid.NewRandom() + if err != nil { + return "", errors.New("Failed saving CA certificate, code 1:\n" + err.Error()) + } + + outputFile := filepath.Join(os.TempDir(), "cert_"+uniqPostfix.String()+".pem") + resp, err := nc.client.R().SetOutput(outputFile).Get("/misc/cert") + if err != nil { + return "", errors.New("Failed saving CA certificate, code 2:\n" + err.Error()) + } + + if resp.IsSuccess() { + return outputFile, nil + } else { + errorMsg, _ := u.FileToString(outputFile) + return "", errors.New("Failed saving CA certificate, code 3\n" + resp.Status() + ": " + errorMsg) + } +} + func (nc *NexusClient) VMAddAccess(vmID, vmName, email string, vmAccessCaps *params.VMAddAccess) error { resp, err := nc.client.R().SetBody(vmAccessCaps).Put("/vms/" + vmID + "/access/" + email) if err != nil { @@ -343,30 +346,6 @@ func (nc *NexusClient) GetListVM(vmID string) (*vm.VMNetworkSerialized, error) { // Private functions //======================================================================================== -// Obtain a public CA certificate for use with a Spice remote viewer. -// The certificate file is saved in the OS' temporary directory. -// Its name is randomly generated and garanteed to be unique. -// It is the caller's responsability to delete it before exiting. -func (nc *NexusClient) getSpiceCertificate() (string, error) { - uniqPostfix, err := uuid.NewRandom() - if err != nil { - return "", errors.New("Failed saving CA certificate: " + err.Error()) - } - - outputFile := filepath.Join(os.TempDir(), "cert_"+uniqPostfix.String()+".pem") - resp, err := nc.client.R().SetOutput(outputFile).Get("/misc/cert") - if err != nil { - return "", errors.New("Failed saving CA certificate: " + err.Error()) - } - - if resp.IsSuccess() { - return outputFile, nil - } else { - errorMsg, _ := u.FileToString(outputFile) - return "", errors.New("Failed saving CA certificate" + resp.Status() + ": " + errorMsg) - } -} - func (nc *NexusClient) vmGetSpiceCreds(vmID string, p params.VMAttachCreds) (*vm.VMSpiceCredentialsSerialized, error) { resp, err := nc.client.R().SetBody(p).Post("/vms/" + vmID + "/spicecreds") if err != nil { -- GitLab