Skip to content
Snippets Groups Projects
Commit bae874cb authored by Florent Gluck's avatar Florent Gluck
Browse files

stateless client: ongoing work on a solution dealing with CA certificate

parent 078123fb
Branches
No related tags found
No related merge requests found
......@@ -34,7 +34,7 @@ func (cmd *Refresh) Run(nc *nc.NexusClient, args []string) int {
return 1
}
err := nc.RefreshAuthToken()
err := nc.RefreshToken()
if err != nil {
u.PrintlnErr(err)
return 1
......
......@@ -70,6 +70,15 @@ var cmdList = []cmd.Command{
&cmdVM.StartWithCreds{Name: "vmstartwithcreds"},
}
var exitFn func()
func exit(code int) {
if exitFn != nil {
exitFn()
}
os.Exit(code)
}
func run() int {
var appname = path.Base(os.Args[0])
clientVersion := version.Get()
......@@ -94,7 +103,11 @@ func run() int {
// return 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) {
......@@ -106,13 +119,23 @@ func run() int {
found, cmd := cmd.Match(cmdName, cmdList)
if found {
exitFn = func() { nc.Close() }
// 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)
}()
if cmdName != "login" {
token, found := os.LookupEnv(g.ENV_NEXUS_TOKEN)
if !found || len(token) == 0 {
u.PrintlnErr("Environment variable \"" + g.ENV_NEXUS_TOKEN + "\" must be set!")
return 1
}
nc.SetAuthToken(token)
nc.AuthenticateWithToken(token)
}
return cmd.Run(nc, cmdArgs)
}
......@@ -122,5 +145,5 @@ func run() int {
}
func main() {
os.Exit(run())
exit(run())
}
......@@ -76,9 +76,17 @@ var cmdList = []cmd.Command{
&cmdVM.StartWithCreds{Name: "vmstartwithcreds"},
}
var exitFn func()
var prompt *liner.State = nil
var savedTermState *term.State = nil
func exit(code int) {
if exitFn != nil {
exitFn()
}
os.Exit(code)
}
func run() int {
var appname = path.Base(os.Args[0])
clientVersion := version.Get()
......@@ -130,20 +138,15 @@ func run() int {
return 1
}
certFile, err := nc.GetVMAttachCertificate()
if err != nil {
u.PrintlnErr("Error: " + err.Error())
return 1
}
exitFn = func() { nc.Close() }
// 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)
exit(1)
}()
shell(nc)
......@@ -211,7 +214,7 @@ Type: "help" for help on commands
found, cmd := cmd.Match(typedCmd, cmdList)
if found {
if typedCmd == "refresh" {
err := nc.RefreshAuthToken()
err := nc.RefreshToken()
if err != nil {
u.PrintlnErr("refresh error: " + err.Error())
break
......@@ -286,5 +289,5 @@ func extractArgs(line string) ([]string, error) {
}
func main() {
os.Exit(run())
exit(run())
}
......@@ -2,15 +2,25 @@ package nexusclient
import (
"crypto/tls"
"encoding/json"
"errors"
"net/url"
"os"
"path/filepath"
"time"
"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/common/params"
"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/libclient/response"
u "gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/libclient/utils"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
)
type NexusClient struct {
client *resty.Client
hostname string // host, e.g. "127.0.0.1" or "my.domain.net"
certFile string // path to the CA certificate tied to the hostname above
// (this cert is required by the spice client)
}
// The host argument has the following format:
......@@ -31,9 +41,61 @@ func New(host string) (*NexusClient, error) {
return nil, err
}
nc.hostname = url.Hostname()
nc.certFile = ""
return nc, nil
}
// Authenticate a user.
// curl --cacert ca-cert.pem -X POST https://localhost:8000/login -H 'Content-Type: application/json' -d '{"email": "johndoe@nexus.org", "pwd":"pipomolo"}'
// Returns an JWT token if authentication succeeded or an error if it failed.
// IMPORTANT: caller MUST call the Close() function before exiting!
func (nc *NexusClient) Authenticate(user, pwd string) (string, error) {
loginArgs := &params.Login{user, pwd}
resp, err := nc.client.R().SetBody(loginArgs).Post("/login")
if err != nil {
return "", err
}
if resp.IsSuccess() {
var token params.Token
err = json.Unmarshal(resp.Body(), &token)
if err != nil {
return "", err
}
nc.client.SetAuthToken(token.Token)
// Download the CA certificate required by Spice clients
nc.certFile, err = nc.downloadVMAttachCert()
if err != nil {
return "", err
}
return token.Token, nil
} else {
return "", response.ErrorToMsg(resp)
}
}
// "Authenticate" by using an already established connection using a previously received token.
// Useful when wanting to access the API without authenticating.
// IMPORTANT: caller MUST call the Close() function before exiting!
func (nc *NexusClient) AuthenticateWithToken(token string) error {
nc.client.SetAuthToken(token)
// Download the CA certificate required by Spice clients
var err error
nc.certFile, err = nc.downloadVMAttachCert()
if err != nil {
return err
}
return nil
}
// Releases/deletes resources allocated by Authenticate and AuthenticateWithToken.
// IMPORTANT: caller MUST call this function before exiting!
func (nc *NexusClient) Close() {
if nc.certFile != "" {
os.Remove(nc.certFile)
}
}
// Returns the hostname, e.g. "127.0.0.1" or "my.domain.net".
func (nc *NexusClient) GetHostname() string {
return nc.hostname
......@@ -43,3 +105,51 @@ func (nc *NexusClient) GetHostname() string {
func (nc *NexusClient) SetTimeout(timeout time.Duration) {
nc.client.SetTimeout(timeout)
}
// Obtain a new JWT token from the server.
func (nc *NexusClient) RefreshToken() error {
resp, err := nc.client.R().Get("/misc/token")
if err != nil {
return err
} else {
if resp.IsSuccess() {
var token params.Token
err = json.Unmarshal(resp.Body(), &token)
if err != nil {
return err
}
nc.client.SetAuthToken(token.Token)
return nil
} else {
return response.ErrorToMsg(resp)
}
}
}
func (nc *NexusClient) getVMAttachCertFile() string {
return nc.certFile
}
// Retrieve and save 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.
func (nc *NexusClient) downloadVMAttachCert() (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)
}
}
package nexusclient
import (
"encoding/json"
"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/common/params"
"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/libclient/response"
)
// Authenticate a user.
// curl --cacert ca-cert.pem -X POST https://localhost:8000/login -H 'Content-Type: application/json' -d '{"email": "johndoe@nexus.org", "pwd":"pipomolo"}'
// Returns an JWT token if authentication succeeded or an error if it failed.
func (nc *NexusClient) Authenticate(user, pwd string) (string, error) {
loginArgs := &params.Login{user, pwd}
resp, err := nc.client.R().SetBody(loginArgs).Post("/login")
if err != nil {
return "", err
}
if resp.IsSuccess() {
var token params.Token
err = json.Unmarshal(resp.Body(), &token)
if err != nil {
return "", err
}
nc.client.SetAuthToken(token.Token)
return token.Token, nil
} else {
return "", response.ErrorToMsg(resp)
}
}
// Obtain a new JWT token from the server.
func (nc *NexusClient) RefreshAuthToken() error {
resp, err := nc.client.R().Get("/misc/token")
if err != nil {
return err
} else {
if resp.IsSuccess() {
var token params.Token
err = json.Unmarshal(resp.Body(), &token)
if err != nil {
return err
}
nc.client.SetAuthToken(token.Token)
return nil
} else {
return response.ErrorToMsg(resp)
}
}
}
// Set the JWT token in the client.
func (nc *NexusClient) SetAuthToken(token string) {
nc.client.SetAuthToken(token)
}
......@@ -2,9 +2,9 @@ package nexusclient
import (
"errors"
"fmt"
"os/exec"
"runtime"
"strconv"
"strings"
)
......@@ -12,7 +12,7 @@ const (
remoteViewerBinary = "remote-viewer"
)
func checkRemoteViewer() error {
func (nc *NexusClient) checkRemoteViewer() error {
os := runtime.GOOS
output, err := exec.Command(remoteViewerBinary, "--version").Output()
if os == "linux" {
......@@ -45,26 +45,25 @@ 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) error {
// portStr := strconv.Itoa(port)
// spice := "spice://" + hostname + "?tls-port=" + portStr + "&password=" + pwd
// spiceCert := "--spice-ca-file=" + cert
// spiceSecure := "--spice-secure-channels=all"
func (nc *NexusClient) runRemoteViewer(name string, port int, pwd string, fullscreen bool) error {
certFile := nc.getVMAttachCertFile()
portStr := strconv.Itoa(port)
spice := "spice://" + nc.GetHostname() + "?tls-port=" + portStr + "&password=" + pwd
spiceCert := "--spice-ca-file=" + certFile
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")
// }
c = exec.Command("remote-viewer", "spice://crap")
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")
}
stdoutStderr, err := c.CombinedOutput()
if err != nil {
return errors.New("err: remote-viewer failed: " + string(stdoutStderr[:]))
}
fmt.Println("err: remote-viewer failed: " + string(stdoutStderr[:]))
return nil
}
......@@ -3,8 +3,6 @@ package nexusclient
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/common/params"
"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/common/vm"
......@@ -12,7 +10,6 @@ import (
u "gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/libclient/utils"
"github.com/go-playground/validator/v10"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
)
const (
......@@ -182,21 +179,21 @@ func (nc *NexusClient) VMStop(vmID string) error {
}
}
func (nc *NexusClient) VMAttachFromID(vmID string, certificateFile string, fullscreen bool) error {
func (nc *NexusClient) VMAttachFromID(vmID string, fullscreen bool) error {
creds, err := nc.vmIdToAttachCreds(vmID)
if err != nil {
return err
}
err = nc.VMAttachFromCreds(*creds, certificateFile, fullscreen)
err = nc.VMAttachFromCreds(*creds, fullscreen)
if err != nil {
return err
}
return nil
}
func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, certificateFile string, fullscreen bool) error {
if err := checkRemoteViewer(); err != nil {
func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, fullscreen bool) error {
if err := nc.checkRemoteViewer(); err != nil {
return err
}
......@@ -206,8 +203,7 @@ func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, cer
return err
}
// First return value (ignored) is of type: stdoutStderr
err = runRemoteViewer(nc.hostname, certificateFile, creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen)
err = nc.runRemoteViewer(creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen)
if err != nil {
return err
}
......@@ -215,8 +211,8 @@ func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, cer
return nil
}
func (nc *NexusClient) VMAttachFromPwd(pwd string, certificateFile string, fullscreen bool) error {
if err := checkRemoteViewer(); err != nil {
func (nc *NexusClient) VMAttachFromPwd(pwd string, fullscreen bool) error {
if err := nc.checkRemoteViewer(); err != nil {
return err
}
......@@ -225,7 +221,7 @@ func (nc *NexusClient) VMAttachFromPwd(pwd string, certificateFile string, fulls
if err != nil {
return err
} else {
err := runRemoteViewer(nc.hostname, certificateFile, creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen)
err := nc.runRemoteViewer(creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen)
if err != nil {
return err
}
......@@ -234,31 +230,6 @@ func (nc *NexusClient) VMAttachFromPwd(pwd string, certificateFile string, fulls
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 {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment