diff options
| author | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2023-04-12 22:55:16 +0200 |
|---|---|---|
| committer | Jeroen Wijenbergh <46386452+jwijenbergh@users.noreply.github.com> | 2023-09-25 09:43:37 +0200 |
| commit | a38e3e79f74e95051db7e14ae14ab817b68b725a (patch) | |
| tree | e26cab53f993d2d845020f81ee6f6f6a8a12ded1 /client/server.go | |
| parent | 4d228ba2084eb810d0cc33308893b00d1bb3eb02 (diff) | |
Refactor: Move client implementation to one file
Much easier to oversee and it forces me to keep the client type as
small as possible. This also uses the cookie for cancellation
We also no longer require tokens inside arguments. We will later
implement them with callbacks
Diffstat (limited to 'client/server.go')
| -rw-r--r-- | client/server.go | 701 |
1 files changed, 0 insertions, 701 deletions
diff --git a/client/server.go b/client/server.go deleted file mode 100644 index b3f7747..0000000 --- a/client/server.go +++ /dev/null @@ -1,701 +0,0 @@ -package client - -import ( - "time" - - "github.com/eduvpn/eduvpn-common/internal/failover" - "github.com/eduvpn/eduvpn-common/internal/http" - "github.com/eduvpn/eduvpn-common/internal/log" - "github.com/eduvpn/eduvpn-common/internal/oauth" - "github.com/eduvpn/eduvpn-common/internal/server" - discotypes "github.com/eduvpn/eduvpn-common/types/discovery" - "github.com/eduvpn/eduvpn-common/types/protocol" - srvtypes "github.com/eduvpn/eduvpn-common/types/server" - "github.com/go-errors/errors" -) - -// TODO: This should not be reliant on an internal type -func getTokens(tok oauth.Token) srvtypes.Tokens { - return srvtypes.Tokens{ - Access: tok.Access, - Refresh: tok.Refresh, - Expires: tok.ExpiredTimestamp.Unix(), - } -} - -// getConfigAuth gets a config with authorization and authentication. -// It also asks for a profile if no valid profile is found. -func (c *Client) getConfigAuth(srv server.Server, preferTCP bool, t srvtypes.Tokens) (*srvtypes.Configuration, error) { - err := c.ensureLogin(srv, t) - if err != nil { - return nil, err - } - - // TODO(jwijenbergh): Should we check if it returns false? - c.FSM.GoTransition(StateRequestConfig) - - ok, err := server.HasValidProfile(srv, c.SupportsWireguard) - if err != nil { - return nil, err - } - - // No valid profile, ask for one - if !ok { - if err = c.askProfile(srv); err != nil { - return nil, err - } - } - - // The profile has been chosen - c.FSM.GoTransition(StateChosenProfile) - - cfg, err := server.Config(srv, c.SupportsWireguard, preferTCP) - if err != nil { - return nil, err - } - - p, err := server.CurrentProfile(srv) - if err != nil { - return nil, err - } - - pCfg := &srvtypes.Configuration{ - VPNConfig: cfg.Config, - Protocol: protocol.New(cfg.Type), - DefaultGateway: p.DefaultGateway, - Tokens: getTokens(cfg.Tokens), - } - - return pCfg, nil -} - -// retryConfigAuth retries the getConfigAuth function if the tokens are invalid. -// If OAuth is cancelled, it makes sure that we only forward the error as additional info. -func (c *Client) retryConfigAuth(srv server.Server, preferTCP bool, t srvtypes.Tokens) (*srvtypes.Configuration, error) { - cfg, err := c.getConfigAuth(srv, preferTCP, t) - if err == nil { - return cfg, nil - } - // Only retry if the error is that the tokens are invalid - tErr := &oauth.TokensInvalidError{} - if errors.As(err, &tErr) { - // TODO: Is passing empty tokens correct here? - cfg, err = c.getConfigAuth(srv, preferTCP, srvtypes.Tokens{}) - if err == nil { - return cfg, nil - } - } - c.goBackInternal() - return nil, err -} - -// getConfig gets an OpenVPN/WireGuard configuration by contacting the server, moving the FSM towards the DISCONNECTED state and then saving the local configuration file. -func (c *Client) getConfig(srv server.Server, preferTCP bool, t srvtypes.Tokens) (*srvtypes.Configuration, error) { - if c.InFSMState(StateDeregistered) { - return nil, errors.Errorf("getConfig attempt in '%v'", StateDeregistered) - } - - // Refresh the server endpoints - // This is the best effort - err := srv.RefreshEndpoints(&c.Discovery) - if err != nil { - log.Logger.Warningf("failed to refresh server endpoints: %v", err) - } - - cfg, err := c.retryConfigAuth(srv, preferTCP, t) - if err != nil { - return nil, err - } - - // Save the config - if err = c.Config.Save(&c); err != nil { - // TODO(jwijenbergh): Not sure why INFO level, yet stacktrace... - // TODO(jwijenbergh): Even worse, why logging it but then return nil? The calling code will think that everything went well. - log.Logger.Infof("c.Config.Save failed: %s\nstacktrace:\n%s", - err.Error(), err.(*errors.Error).ErrorStack()) - } - - c.FSM.GoTransition(StateGotConfig) - - return cfg, nil -} - -// Cleanup cleans up the VPN connection by sending a /disconnect to the server -func (c *Client) Cleanup(ct srvtypes.Tokens) error { - srv, err := c.Servers.GetCurrentServer() - if err != nil { - c.logError(err) - return err - } - err = srv.RefreshEndpoints(&c.Discovery) - if err != nil { - log.Logger.Warningf("failed to refresh server endpoints: %v", err) - } - - // If we need to relogin, update tokens - if server.NeedsRelogin(srv) { - server.UpdateTokens(srv, oauth.Token{ - Access: ct.Access, - Refresh: ct.Refresh, - ExpiredTimestamp: time.Unix(ct.Expires, 0), - }) - } - // update tokens to client - defer c.ForwardTokenUpdate(srv) - // Do the /disconnect API call - err = server.Disconnect(srv) - if err != nil { - // We log nothing here because this can happen regularly - // Maybe we should not log errors that we return directly anyways? - return err - } - // TODO: Tokens might be refreshed, return updated tokens - // Not implemented yet, because ideally we want this implemented with an interface - return nil -} - -// SetSecureLocation sets the location for the current secure location server. countryCode is the secure location to be chosen. -// This function returns an error e.g. if the server cannot be found or the location is wrong. -func (c *Client) SetSecureLocation(countryCode string) error { - if c.InFSMState(StateAskLocation) { - defer c.locationWg.Done() - } - // Not supported with Let's Connect! - if c.isLetsConnect() { - err := errors.Errorf("discovery with Let's Connect is not supported") - c.logError(err) - return err - } - - srv, err := c.Discovery.ServerByCountryCode(countryCode) - if err != nil { - c.goBackInternal() - c.logError(err) - return err - } - - if err = c.Servers.SetSecureLocation(srv); err != nil { - c.goBackInternal() - c.logError(err) - } - - return err -} - -// RemoveSecureInternet removes the current secure internet server. -// It returns an error if the server cannot be removed due to the state being DEREGISTERED. -// Note that if the server does not exist, it returns nil as an error. -func (c *Client) RemoveSecureInternet() error { - if c.InFSMState(StateDeregistered) { - err := errors.Errorf("RemoveSecureInternet attempt in '%v'", StateDeregistered) - c.logError(err) - return err - } - // No error because we can only have one secure internet server and if there are no secure internet servers, this is a NO-OP - c.Servers.RemoveSecureInternet() - c.FSM.GoTransition(StateNoServer) - // Save the config - if err := c.Config.Save(&c); err != nil { - // TODO(jwijenbergh): Not sure why INFO level, yet stacktrace... - // TODO(jwijenbergh): Even worse, why logging it but then return nil? The calling code will think that everything went well. - log.Logger.Infof("c.Config.Save failed: %s\nstacktrace:\n%s", - err.Error(), err.(*errors.Error).ErrorStack()) - } - return nil -} - -// RemoveInstituteAccess removes the institute access server with `url`. -// It returns an error if the server cannot be removed due to the state being DEREGISTERED. -// Note that if the server does not exist, it returns nil as an error. -func (c *Client) RemoveInstituteAccess(url string) error { - if c.InFSMState(StateDeregistered) { - err := errors.Errorf("RemoveInstituteAccess attempt in '%v'", StateDeregistered) - c.logError(err) - return err - } - // No error because this is a NO-OP if the server doesn't exist - c.Servers.RemoveInstituteAccess(url) - c.FSM.GoTransition(StateNoServer) - // Save the config - if err := c.Config.Save(&c); err != nil { - // TODO(jwijenbergh): Not sure why INFO level, yet stacktrace... - // TODO(jwijenbergh): Even worse, why logging it but then return nil? The calling code will think that everything went well. - log.Logger.Infof("c.Config.Save failed: %s\nstacktrace:\n%s", - err.Error(), err.(*errors.Error).ErrorStack()) - } - return nil -} - -// RemoveCustomServer removes the custom server with `url`. -// It returns an error if the server cannot be removed due to the state being DEREGISTERED. -// Note that if the server does not exist, it returns nil as an error. -func (c *Client) RemoveCustomServer(url string) error { - if c.InFSMState(StateDeregistered) { - err := errors.Errorf("RemoveCustomServer attempt in '%v'", StateDeregistered) - c.logError(err) - return err - } - // No error because this is a NO-OP if the server doesn't exist - c.Servers.RemoveCustomServer(url) - c.FSM.GoTransition(StateNoServer) - // Save the config - if err := c.Config.Save(&c); err != nil { - // TODO(jwijenbergh): Not sure why INFO level, yet stacktrace... - // TODO(jwijenbergh): Even worse, why logging it but then return nil? The calling code will think that everything went well. - log.Logger.Infof("c.Config.Save failed: %s\nstacktrace:\n%s", - err.Error(), err.(*errors.Error).ErrorStack()) - } - return nil -} - -// AddInstituteServer adds an Institute Access server by `url`. -func (c *Client) AddInstituteServer(url string) (err error) { - defer func() { - if err != nil { - c.logError(err) - } - }() - - // Not supported with Let's Connect! - if c.isLetsConnect() { - return errors.Errorf("adding and Institute Access server with Let's Connect is not supported") - } - - // Indicate that we're loading the server - c.FSM.GoTransition(StateLoadingServer) - - // Check if we are able to fetch discovery, and log if something went wrong - if _, err := c.DiscoServers(); err != nil { - log.Logger.Warningf("Failed to get discovery servers: %v", err) - } - - if _, err := c.DiscoOrganizations(); err != nil { - log.Logger.Warningf("Failed to get discovery organizations: %v", err) - } - - // FIXME: Do nothing with discovery here as the client already has it - // So pass a server as the parameter - var dSrv *discotypes.Server - dSrv, err = c.Discovery.ServerByURL(url, "institute_access") - if err != nil { - c.goBackInternal() - return err - } - - // Add the secure internet server - srv, err := c.Servers.AddInstituteAccessServer(dSrv) - if err != nil { - c.goBackInternal() - return err - } - - // Set the server as the current so OAuth can be cancelled - if err = c.Servers.SetInstituteAccess(srv); err != nil { - c.goBackInternal() - return err - } - - // Indicate that we want to authorize this server - c.FSM.GoTransition(StateChosenServer) - - // Authorize it - if err = c.ensureLogin(srv, srvtypes.Tokens{}); err != nil { - // Removing is best effort - _ = c.RemoveInstituteAccess(url) - return err - } - - c.FSM.GoTransition(StateNoServer) - return nil -} - -// AddSecureInternetHomeServer adds a Secure Internet Home Server with `orgID` that was obtained from the Discovery file. -// Because there is only one Secure Internet Home Server, it replaces the existing one. -func (c *Client) AddSecureInternetHomeServer(orgID string) (err error) { - defer func() { - if err != nil { - c.logError(err) - } - }() - - // Not supported with Let's Connect! - if c.isLetsConnect() { - return errors.Errorf("adding a secure internet server with Let's Connect is not supported") - } - - // Indicate that we're loading the server - c.FSM.GoTransition(StateLoadingServer) - - // Check if we are able to fetch discovery, and log if something went wrong - if _, err := c.DiscoServers(); err != nil { - log.Logger.Warningf("Failed to get discovery servers: %v", err) - } - - if _, err := c.DiscoOrganizations(); err != nil { - log.Logger.Warningf("Failed to get discovery organizations: %v", err) - } - - // Get the secure internet URL from discovery - org, dSrv, err := c.Discovery.SecureHomeArgs(orgID) - if err != nil { - // We mark the organizations as expired because we got an error - // Note that in the docs it states that it only should happen when the Org ID doesn't exist - // However, this is nice as well because it also catches the error where the SecureInternetHome server is not found - c.Discovery.MarkOrganizationsExpired() - c.goBackInternal() - return err - } - - // Add the secure internet server - srv, err := c.Servers.AddSecureInternet(org, dSrv) - if err != nil { - c.goBackInternal() - return err - } - - // TODO(jwijenbergh): Does this call transfers execution flow to UI? - if err = c.askSecureLocation(); err != nil { - // Removing is the best effort - // This already goes back to the main screen - _ = c.RemoveSecureInternet() - return err - } - - c.FSM.GoTransition(StateChosenLocation) - - // Set the server as the current so OAuth can be cancelled - if err = c.Servers.SetSecureInternet(srv); err != nil { - c.goBackInternal() - return err - } - - // Server has been chosen for authentication - c.FSM.GoTransition(StateChosenServer) - - // Authorize it - if err = c.ensureLogin(srv, srvtypes.Tokens{}); err != nil { - // Removing is best effort - _ = c.RemoveSecureInternet() - return err - } - c.FSM.GoTransition(StateNoServer) - return nil -} - -// AddCustomServer adds a Custom Server by `url`. -func (c *Client) AddCustomServer(url string) (err error) { - defer func() { - if err != nil { - c.logError(err) - } - }() - - if url, err = http.EnsureValidURL(url); err != nil { - return err - } - - // Indicate that we're loading the server - c.FSM.GoTransition(StateLoadingServer) - - customServer := &discotypes.Server{ - BaseURL: url, - DisplayName: map[string]string{"en": url}, - Type: "custom_server", - } - - // A custom server is just an institute access server under the hood - srv, err := c.Servers.AddCustomServer(customServer) - if err != nil { - c.goBackInternal() - return err - } - - // Set the server as the current so OAuth can be cancelled - if err = c.Servers.SetCustomServer(srv); err != nil { - c.goBackInternal() - return err - } - - // Server has been chosen for authentication - c.FSM.GoTransition(StateChosenServer) - - // Authorize it - if err = c.ensureLogin(srv, srvtypes.Tokens{}); err != nil { - // removing is best effort - _ = c.RemoveCustomServer(url) - return err - } - - c.FSM.GoTransition(StateNoServer) - return nil -} - -// GetConfigInstituteAccess gets a configuration for an Institute Access Server. -// It ensures that the Institute Access Server exists by creating or using an existing one with the url. -// `preferTCP` indicates that the client wants to use TCP (through OpenVPN) to establish the VPN tunnel. -func (c *Client) GetConfigInstituteAccess(url string, preferTCP bool, t srvtypes.Tokens) (cfg *srvtypes.Configuration, err error) { - defer func() { - if err != nil { - c.logError(err) - } - }() - - // Not supported with Let's Connect! - if c.isLetsConnect() { - return nil, errors.Errorf("discovery with Let's Connect is not supported") - } - - c.FSM.GoTransition(StateLoadingServer) - - // Get the server if it exists - var srv *server.InstituteAccessServer - if srv, err = c.Servers.GetInstituteAccess(url); err != nil { - c.goBackInternal() - return nil, err - } - - // Set the server as the current - if err = c.Servers.SetInstituteAccess(srv); err != nil { - return nil, err - } - - // The server has now been chosen - c.FSM.GoTransition(StateChosenServer) - - if cfg, err = c.getConfig(srv, preferTCP, t); err != nil { - c.goBackInternal() - } - - // Also forward tokens using the callback - c.ForwardTokenUpdate(srv) - - return cfg, err -} - -// GetConfigSecureInternet gets a configuration for a Secure Internet Server. -// It ensures that the Secure Internet Server exists by creating or using an existing one with the orgID. -// `preferTCP` indicates that the client wants to use TCP (through OpenVPN) to establish the VPN tunnel. -// TODO: Check on first argument orgID -func (c *Client) GetConfigSecureInternet(_ string, preferTCP bool, t srvtypes.Tokens) (cfg *srvtypes.Configuration, err error) { - defer func() { - if err != nil { - c.logError(err) - } - }() - - log.Logger.Debugf("getting config for secure internet server with org ID: '%s", orgID) - - // Not supported with Let's Connect! - if c.isLetsConnect() { - return nil, errors.Errorf("discovery with Let's Connect is not supported") - } - - c.FSM.GoTransition(StateLoadingServer) - - // Get the server if it exists - var srv *server.SecureInternetHomeServer - if srv, err = c.Servers.GetSecureInternetHomeServer(); err != nil { - c.goBackInternal() - return nil, err - } - - // Set the server as the current - if err = c.Servers.SetSecureInternet(srv); err != nil { - return nil, err - } - - c.FSM.GoTransition(StateChosenServer) - - if cfg, err = c.getConfig(srv, preferTCP, t); err != nil { - c.goBackInternal() - } - - // Also forward tokens using the callback - c.ForwardTokenUpdate(srv) - - return cfg, err -} - -// GetConfigCustomServer gets a configuration for a Custom Server. -// It ensures that the Custom Server exists by creating or using an existing one with the url. -// `preferTCP` indicates that the client wants to use TCP (through OpenVPN) to establish the VPN tunnel. -func (c *Client) GetConfigCustomServer(url string, preferTCP bool, t srvtypes.Tokens) (cfg *srvtypes.Configuration, err error) { - defer func() { - if err != nil { - c.logError(err) - } - }() - - if url, err = http.EnsureValidURL(url); err != nil { - return nil, err - } - - c.FSM.GoTransition(StateLoadingServer) - - // Get the server if it exists - var srv *server.InstituteAccessServer - if srv, err = c.Servers.GetCustomServer(url); err != nil { - c.goBackInternal() - return nil, err - } - - // Set the server as the current - if err = c.Servers.SetCustomServer(srv); err != nil { - c.goBackInternal() - return nil, err - } - - c.FSM.GoTransition(StateChosenServer) - - if cfg, err = c.getConfig(srv, preferTCP, t); err != nil { - c.goBackInternal() - } - - // Also forward tokens using the callback - c.ForwardTokenUpdate(srv) - - return cfg, err -} - -// askSecureLocation asks the user to choose a Secure Internet location by moving the FSM to the STATE_ASK_LOCATION state. -func (c *Client) askSecureLocation() error { - loc := c.Discovery.SecureLocationList() - - c.locationWg.Add(1) - // Ask for the location in the callback - if err := c.FSM.GoTransitionRequired(StateAskLocation, loc); err != nil { - return err - } - - c.locationWg.Wait() - - // The state has changed, meaning setting the secure location was not successful - if c.FSM.Current != StateAskLocation { - log.Logger.Debugf("fsm failed to transit; expected %v / actual %v", GetStateName(StateAskLocation), GetStateName(c.FSM.Current)) - return errors.New("failed loading secure internet location") - } - return nil -} - -// RenewSession renews the session for the current VPN server. -// This logs the user back in. -func (c *Client) RenewSession() (err error) { - defer func() { - if err != nil { - c.logError(err) - } - }() - - var srv server.Server - if srv, err = c.Servers.GetCurrentServer(); err != nil { - return err - } - - err = srv.RefreshEndpoints(&c.Discovery) - if err != nil { - log.Logger.Warningf("failed to refresh server endpoints: %v", err) - } - - // The server has not been chosen yet, this means that we want to manually renew - if !c.FSM.InState(StateChosenServer) { - c.FSM.GoTransition(StateLoadingServer) - c.FSM.GoTransition(StateChosenServer) - } - - server.MarkTokensForRenew(srv) - return c.ensureLogin(srv, srvtypes.Tokens{}) -} - -// ensureLogin logs the user back in if needed. -// It runs the FSM transitions to ask for user input. -func (c *Client) ensureLogin(srv server.Server, t srvtypes.Tokens) (err error) { - // Relogin with oauth - // This moves the state to authorized - if !server.NeedsRelogin(srv) { - // OAuth was valid, ensure we are in the authorized state - c.FSM.GoTransition(StateAuthorized) - return nil - } - - // Try again but update the tokens using the client provided tokens - server.UpdateTokens(srv, oauth.Token{ - Access: t.Access, - Refresh: t.Refresh, - ExpiredTimestamp: time.Unix(t.Expires, 0), - }) - if !server.NeedsRelogin(srv) { - // OAuth was valid, ensure we are in the authorized state - c.FSM.GoTransition(StateAuthorized) - return nil - } - - // Mark organizations as expired if the server is a secure internet server - b, err := srv.Base() - // We only try to update it when we found the server base - if err == nil && b.Type == "secure_internet" { - c.Discovery.MarkOrganizationsExpired() - } - - // Tokens are not valid or the client gave an error when updating tokens - // Otherwise, do the OAuth exchange - var url string - if url, err = server.OAuthURL(srv, c.Name); err != nil { - return err - } - - if err = c.FSM.GoTransitionRequired(StateOAuthStarted, url); err != nil { - return err - } - - if err = server.OAuthExchange(srv); err != nil { - c.goBackInternal() - } - c.FSM.GoTransition(StateAuthorized) - - return err -} - -// SetProfileID sets a `profileID` for the current server. -// An error is returned if this is not possible, for example when no server is configured. -func (c *Client) SetProfileID(profileID string) (err error) { - if c.InFSMState(StateAskProfile) { - defer c.profileWg.Done() - } - defer func() { - if err != nil { - c.logError(err) - } - }() - - var srv server.Server - if srv, err = c.Servers.GetCurrentServer(); err != nil { - c.goBackInternal() - return err - } - - var b *server.Base - if b, err = srv.Base(); err != nil { - c.goBackInternal() - return err - } - b.Profiles.Current = profileID - - return nil -} - -func (c *Client) StartFailover(gateway string, wgMTU int, readRxBytes func() (int64, error)) (bool, error) { - if c.Failover != nil { - return false, errors.New("another failover process is already started") - } - c.Failover = failover.New(readRxBytes) - - return c.Failover.Start(gateway, wgMTU) -} - -func (c *Client) CancelFailover() error { - if c.Failover == nil { - return errors.New("no failover process") - } - c.Failover.Cancel() - return nil -} |
