diff options
| author | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-10-19 11:35:30 +0200 |
|---|---|---|
| committer | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-10-19 11:35:30 +0200 |
| commit | 5382a54af02d0a0aa6903192427930e25aef032c (patch) | |
| tree | c891897c097b16e97ed6cb8e7a4623e646c29855 | |
| parent | eea454c977513c4be67d6f1efb3aa01b43b86690 (diff) | |
Client: Separate into different files
| -rw-r--r-- | client/client.go | 929 | ||||
| -rw-r--r-- | client/fsm.go | 262 | ||||
| -rw-r--r-- | client/server.go | 681 |
3 files changed, 943 insertions, 929 deletions
diff --git a/client/client.go b/client/client.go index 4cf95cd..2808426 100644 --- a/client/client.go +++ b/client/client.go @@ -1,7 +1,6 @@ package client import ( - "errors" "fmt" "strings" @@ -9,7 +8,6 @@ import ( "github.com/eduvpn/eduvpn-common/internal/discovery" "github.com/eduvpn/eduvpn-common/internal/fsm" "github.com/eduvpn/eduvpn-common/internal/log" - "github.com/eduvpn/eduvpn-common/internal/oauth" "github.com/eduvpn/eduvpn-common/internal/server" "github.com/eduvpn/eduvpn-common/internal/util" "github.com/eduvpn/eduvpn-common/types" @@ -143,202 +141,6 @@ func (client *Client) Deregister() { *client = Client{} } -// goBackInternal uses the public go back but logs an error if it happened. -func (client *Client) goBackInternal() { - goBackErr := client.GoBack() - if goBackErr != nil { - client.Logger.Info( - fmt.Sprintf( - "Failed going back, error: %s", - types.GetErrorTraceback(goBackErr), - ), - ) - } -} - -// GoBack transitions the FSM back to the previous UI state, for now this is always the NO_SERVER state. -func (client *Client) GoBack() error { - errorMessage := "failed to go back" - if client.InFSMState(STATE_DEREGISTERED) { - client.Logger.Error("Wrong state, cannot go back when deregistered") - return &types.WrappedErrorMessage{ - Message: errorMessage, - Err: FSMDeregisteredError{}.CustomError(), - } - } - - // FIXME: Abitrary back transitions don't work because we need the approriate data - client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) - return nil -} - -// ensureLogin logs the user back in if needed. -// It runs the FSM transitions to ask for user input. -func (client *Client) ensureLogin(chosenServer server.Server) error { - errorMessage := "failed ensuring login" - // Relogin with oauth - // This moves the state to authorized - if server.NeedsRelogin(chosenServer) { - url, urlErr := server.GetOAuthURL(chosenServer, client.Name) - - client.FSM.GoTransitionWithData(STATE_OAUTH_STARTED, url, true) - - if urlErr != nil { - client.goBackInternal() - return &types.WrappedErrorMessage{Message: errorMessage, Err: urlErr} - } - - exchangeErr := server.OAuthExchange(chosenServer) - - if exchangeErr != nil { - client.goBackInternal() - return &types.WrappedErrorMessage{Message: errorMessage, Err: exchangeErr} - } - } - // OAuth was valid, ensure we are in the authorized state - client.FSM.GoTransition(STATE_AUTHORIZED) - return nil -} - -// getConfigAuth gets a config with authorization and authentication. -// It also asks for a profile if no valid profile is found. -func (client *Client) getConfigAuth( - chosenServer server.Server, - preferTCP bool, -) (string, string, error) { - loginErr := client.ensureLogin(chosenServer) - if loginErr != nil { - return "", "", loginErr - } - client.FSM.GoTransition(STATE_REQUEST_CONFIG) - - validProfile, profileErr := server.HasValidProfile(chosenServer) - if profileErr != nil { - return "", "", profileErr - } - - // No valid profile, ask for one - if !validProfile { - askProfileErr := client.askProfile(chosenServer) - if askProfileErr != nil { - return "", "", askProfileErr - } - } - - // We return the error otherwise we wrap it too much - return server.GetConfig(chosenServer, preferTCP) -} - -// 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 (client *Client) retryConfigAuth( - chosenServer server.Server, - preferTCP bool, -) (string, string, error) { - errorMessage := "failed authorized config retry" - config, configType, configErr := client.getConfigAuth(chosenServer, preferTCP) - if configErr != nil { - level := types.ERR_OTHER - var error *oauth.OAuthTokensInvalidError - var oauthCancelledError *oauth.OAuthCancelledCallbackError - - // Only retry if the error is that the tokens are invalid - if errors.As(configErr, &error) { - config, configType, configErr = client.getConfigAuth( - chosenServer, - preferTCP, - ) - if configErr == nil { - return config, configType, nil - } - } - if errors.As(configErr, &oauthCancelledError) { - level = types.ERR_INFO - } - client.goBackInternal() - return "", "", &types.WrappedErrorMessage{Level: level, Message: errorMessage, Err: configErr} - } - return config, configType, nil -} - -// 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 (client *Client) getConfig( - chosenServer server.Server, - preferTCP bool, -) (string, string, error) { - errorMessage := "failed to get a configuration for OpenVPN/Wireguard" - if client.InFSMState(STATE_DEREGISTERED) { - return "", "", &types.WrappedErrorMessage{ - Message: errorMessage, - Err: FSMDeregisteredError{}.CustomError(), - } - } - - config, configType, configErr := client.retryConfigAuth(chosenServer, preferTCP) - - if configErr != nil { - return "", "", &types.WrappedErrorMessage{Level: types.GetErrorLevel(configErr), Message: errorMessage, Err: configErr} - } - - currentServer, currentServerErr := client.Servers.GetCurrentServer() - if currentServerErr != nil { - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} - } - - // Signal the server display info - client.FSM.GoTransitionWithData(STATE_DISCONNECTED, currentServer, false) - - // Save the config - saveErr := client.Config.Save(&client) - if saveErr != nil { - client.Logger.Info( - fmt.Sprintf( - "Failed saving configuration after getting a server: %s", - types.GetErrorTraceback(saveErr), - ), - ) - } - - return config, configType, 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 (client *Client) SetSecureLocation(countryCode string) error { - errorMessage := "failed asking secure location" - - // Not supported with Let's Connect! - if client.isLetsConnect() { - return &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} - } - - server, serverErr := client.Discovery.GetServerByCountryCode(countryCode, "secure_internet") - if serverErr != nil { - client.Logger.Error( - fmt.Sprintf( - "Failed getting secure internet server by country code: %s with error: %s", - countryCode, - types.GetErrorTraceback(serverErr), - ), - ) - client.goBackInternal() - return &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} - } - - setLocationErr := client.Servers.SetSecureLocation(server) - if setLocationErr != nil { - client.Logger.Error( - fmt.Sprintf( - "Failed setting secure internet server with error: %s", - types.GetErrorTraceback(setLocationErr), - ), - ) - client.goBackInternal() - return &types.WrappedErrorMessage{Message: errorMessage, Err: setLocationErr} - } - return nil -} - // askProfile asks the user for a profile by moving the FSM to the ASK_PROFILE state. func (client *Client) askProfile(chosenServer server.Server) error { base, baseErr := chosenServer.GetBase() @@ -349,453 +151,6 @@ func (client *Client) askProfile(chosenServer server.Server) error { return nil } -// askSecureLocation asks the user to choose a Secure Internet location by moving the FSM to the STATE_ASK_LOCATION state. -func (client *Client) askSecureLocation() error { - locations := client.Discovery.GetSecureLocationList() - - // Ask for the location in the callback - client.FSM.GoTransitionWithData(STATE_ASK_LOCATION, locations, false) - - // The state has changed, meaning setting the secure location was not successful - if client.FSM.Current != STATE_ASK_LOCATION { - // TODO: maybe a custom type for this errors.new? - return &types.WrappedErrorMessage{ - Message: "failed setting secure location", - Err: errors.New("failed loading secure location"), - } - } - return nil -} - -// 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 (client *Client) RemoveSecureInternet() error { - if client.InFSMState(STATE_DEREGISTERED) { - client.Logger.Error("Failed removing secure internet server due to deregistered") - return &types.WrappedErrorMessage{ - Message: "failed to remove Secure Internet", - Err: FSMDeregisteredError{}.CustomError(), - } - } - // No error because we can only have one secure internet server and if there are no secure internet servers, this is a NO-OP - client.Servers.RemoveSecureInternet() - client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) - // Save the config - saveErr := client.Config.Save(&client) - if saveErr != nil { - client.Logger.Info( - fmt.Sprintf( - "Failed saving configuration after removing a secure internet server: %s", - types.GetErrorTraceback(saveErr), - ), - ) - } - 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 (client *Client) RemoveInstituteAccess(url string) error { - if client.InFSMState(STATE_DEREGISTERED) { - return &types.WrappedErrorMessage{ - Message: "failed to remove Institute Access", - Err: FSMDeregisteredError{}.CustomError(), - } - } - // No error because this is a NO-OP if the server doesn't exist - client.Servers.RemoveInstituteAccess(url) - client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) - // Save the config - saveErr := client.Config.Save(&client) - if saveErr != nil { - client.Logger.Info( - fmt.Sprintf( - "Failed saving configuration after removing an institute access server: %s", - types.GetErrorTraceback(saveErr), - ), - ) - } - 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 (client *Client) RemoveCustomServer(url string) error { - if client.InFSMState(STATE_DEREGISTERED) { - return &types.WrappedErrorMessage{ - Message: "failed to remove Custom Server", - Err: FSMDeregisteredError{}.CustomError(), - } - } - // No error because this is a NO-OP if the server doesn't exist - client.Servers.RemoveCustomServer(url) - client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) - // Save the config - saveErr := client.Config.Save(&client) - if saveErr != nil { - client.Logger.Info( - fmt.Sprintf( - "Failed saving configuration after removing a custom server: %s", - types.GetErrorTraceback(saveErr), - ), - ) - } - return nil -} - -// AddInstituteServer adds an Institute Access server by `url`. -func (client *Client) AddInstituteServer(url string) (server.Server, error) { - errorMessage := fmt.Sprintf("failed adding Institute Access server with url %s", url) - - // Not supported with Let's Connect! - if client.isLetsConnect() { - return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} - } - - // Indicate that we're loading the server - client.FSM.GoTransition(STATE_LOADING_SERVER) - - // FIXME: Do nothing with discovery here as the client already has it - // So pass a server as the parameter - instituteServer, discoErr := client.Discovery.GetServerByURL(url, "institute_access") - if discoErr != nil { - client.goBackInternal() - return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: discoErr} - } - - // Add the secure internet server - server, serverErr := client.Servers.AddInstituteAccessServer(instituteServer) - if serverErr != nil { - client.goBackInternal() - return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} - } - - // Indicate that we want to authorize this server - client.FSM.GoTransition(STATE_CHOSEN_SERVER) - - // Authorize it - loginErr := client.ensureLogin(server) - if loginErr != nil { - // Removing is best effort - _ = client.RemoveInstituteAccess(url) - return nil, &types.WrappedErrorMessage{Level: types.GetErrorLevel(loginErr), Message: errorMessage, Err: loginErr} - } - - client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) - return server, 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 (client *Client) AddSecureInternetHomeServer(orgID string) (server.Server, error) { - errorMessage := fmt.Sprintf( - "failed adding Secure Internet home server with organization ID %s", - orgID, - ) - - // Not supported with Let's Connect! - if client.isLetsConnect() { - return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} - } - - // Indicate that we're loading the server - client.FSM.GoTransition(STATE_LOADING_SERVER) - - // Get the secure internet URL from discovery - secureOrg, secureServer, discoErr := client.Discovery.GetSecureHomeArgs(orgID) - if discoErr != nil { - client.goBackInternal() - return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: discoErr} - } - - // Add the secure internet server - server, serverErr := client.Servers.AddSecureInternet(secureOrg, secureServer) - if serverErr != nil { - client.goBackInternal() - return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} - } - - locationErr := client.askSecureLocation() - if locationErr != nil { - // Removing is best effort - _ = client.RemoveSecureInternet() - return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: locationErr} - } - - // Server has been chosen for authentication - client.FSM.GoTransition(STATE_CHOSEN_SERVER) - - // Authorize it - loginErr := client.ensureLogin(server) - if loginErr != nil { - // Removing is best effort - _ = client.RemoveSecureInternet() - return nil, &types.WrappedErrorMessage{Level: types.GetErrorLevel(loginErr), Message: errorMessage, Err: loginErr} - } - client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) - return server, nil -} - -// AddCustomServer adds a Custom Server by `url` -func (client *Client) AddCustomServer(url string) (server.Server, error) { - errorMessage := fmt.Sprintf("failed adding Custom server with url %s", url) - - url, urlErr := util.EnsureValidURL(url) - if urlErr != nil { - return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: urlErr} - } - - // Indicate that we're loading the server - client.FSM.GoTransition(STATE_LOADING_SERVER) - - customServer := &types.DiscoveryServer{ - BaseURL: url, - DisplayName: map[string]string{"en": url}, - Type: "custom_server", - } - - // A custom server is just an institute access server under the hood - server, serverErr := client.Servers.AddCustomServer(customServer) - if serverErr != nil { - client.goBackInternal() - return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} - } - - // Server has been chosen for authentication - client.FSM.GoTransition(STATE_CHOSEN_SERVER) - - // Authorize it - loginErr := client.ensureLogin(server) - if loginErr != nil { - // removing is best effort - _ = client.RemoveCustomServer(url) - return nil, &types.WrappedErrorMessage{Level: types.GetErrorLevel(loginErr), Message: errorMessage, Err: loginErr} - } - - client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) - return server, 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 (client *Client) GetConfigInstituteAccess(url string, preferTCP bool) (string, string, error) { - errorMessage := fmt.Sprintf("failed getting a configuration for Institute Access %s", url) - - // Not supported with Let's Connect! - if client.isLetsConnect() { - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} - } - - client.FSM.GoTransition(STATE_LOADING_SERVER) - - // Get the server if it exists - server, serverErr := client.Servers.GetInstituteAccess(url) - if serverErr != nil { - client.Logger.Error( - fmt.Sprintf( - "Failed getting an institute access server configuration with error: %s", - types.GetErrorTraceback(serverErr), - ), - ) - client.goBackInternal() - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} - } - - // Set the server as the current - currentErr := client.Servers.SetInstituteAccess(server) - if currentErr != nil { - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: currentErr} - } - - // The server has now been chosen - client.FSM.GoTransition(STATE_CHOSEN_SERVER) - - config, configType, configErr := client.getConfig(server, preferTCP) - if configErr != nil { - client.Logger.Inherit(configErr, - fmt.Sprintf( - "Failed getting an institute access server configuration with error: %s", - types.GetErrorTraceback(configErr), - ), - ) - client.goBackInternal() - return "", "", &types.WrappedErrorMessage{Level: types.GetErrorLevel(configErr), Message: errorMessage, Err: configErr} - } - return config, configType, nil -} - -// 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. -func (client *Client) GetConfigSecureInternet( - orgID string, - preferTCP bool, -) (string, string, error) { - errorMessage := fmt.Sprintf( - "failed getting a configuration for Secure Internet organization %s", - orgID, - ) - - // Not supported with Let's Connect! - if client.isLetsConnect() { - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} - } - - client.FSM.GoTransition(STATE_LOADING_SERVER) - - // Get the server if it exists - server, serverErr := client.Servers.GetSecureInternetHomeServer() - if serverErr != nil { - client.Logger.Error( - fmt.Sprintf( - "Failed getting a custom server configuration with error: %s", - types.GetErrorTraceback(serverErr), - ), - ) - client.goBackInternal() - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} - } - - // Set the server as the current - currentErr := client.Servers.SetSecureInternet(server) - if currentErr != nil { - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: currentErr} - } - - client.FSM.GoTransition(STATE_CHOSEN_SERVER) - - config, configType, configErr := client.getConfig(server, preferTCP) - if configErr != nil { - client.Logger.Inherit( - configErr, - fmt.Sprintf( - "Failed getting a secure internet configuration with error: %s", - types.GetErrorTraceback(configErr), - ), - ) - client.goBackInternal() - return "", "", &types.WrappedErrorMessage{Level: types.GetErrorLevel(configErr), Message: errorMessage, Err: configErr} - } - return config, configType, nil -} - -// 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 (client *Client) GetConfigCustomServer(url string, preferTCP bool) (string, string, error) { - errorMessage := fmt.Sprintf("failed getting a configuration for custom server %s", url) - - url, urlErr := util.EnsureValidURL(url) - if urlErr != nil { - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: urlErr} - } - - client.FSM.GoTransition(STATE_LOADING_SERVER) - - // Get the server if it exists - server, serverErr := client.Servers.GetCustomServer(url) - if serverErr != nil { - client.Logger.Error( - fmt.Sprintf( - "Failed getting a custom server configuration with error: %s", - types.GetErrorTraceback(serverErr), - ), - ) - client.goBackInternal() - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} - } - - // Set the server as the current - currentErr := client.Servers.SetCustomServer(server) - if currentErr != nil { - return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: currentErr} - } - - client.FSM.GoTransition(STATE_CHOSEN_SERVER) - - config, configType, configErr := client.getConfig(server, preferTCP) - if configErr != nil { - client.Logger.Inherit( - configErr, - fmt.Sprintf( - "Failed getting a custom server with error: %s", - types.GetErrorTraceback(configErr), - ), - ) - client.goBackInternal() - return "", "", &types.WrappedErrorMessage{Level: types.GetErrorLevel(configErr), Message: errorMessage, Err: configErr} - } - return config, configType, nil -} - -// CancelOAuth cancels OAuth if one is in progress. -// If OAuth is not in progress, it returns an error. -// An error is also returned if OAuth is in progress but it fails to cancel it. -func (client *Client) CancelOAuth() error { - errorMessage := "failed to cancel OAuth" - if !client.InFSMState(STATE_OAUTH_STARTED) { - client.Logger.Error("Failed cancelling OAuth, not in the right state") - return &types.WrappedErrorMessage{ - Message: errorMessage, - Err: FSMWrongStateError{ - Got: client.FSM.Current, - Want: STATE_OAUTH_STARTED, - }.CustomError(), - } - } - - currentServer, serverErr := client.Servers.GetCurrentServer() - if serverErr != nil { - client.Logger.Warning( - fmt.Sprintf( - "Failed cancelling OAuth, no server configured to cancel OAuth for (err: %v)", - serverErr, - ), - ) - return &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} - } - server.CancelOAuth(currentServer) - return nil -} - -// ChangeSecureLocation changes the location for an existing Secure Internet Server. -// Changing a secure internet location is only possible when the user is in the main screen (STATE_NO_SERVER), otherwise it returns an error. -// It also returns an error if something has gone wrong when selecting the new location -func (client *Client) ChangeSecureLocation() error { - errorMessage := "failed to change location from the main screen" - - if !client.InFSMState(STATE_NO_SERVER) { - client.Logger.Error("Failed changing secure internet location, not in the right state") - return &types.WrappedErrorMessage{ - Message: errorMessage, - Err: FSMWrongStateError{ - Got: client.FSM.Current, - Want: STATE_NO_SERVER, - }.CustomError(), - } - } - - askLocationErr := client.askSecureLocation() - if askLocationErr != nil { - client.Logger.Error( - fmt.Sprintf( - "Failed changing secure internet location, err: %s", - types.GetErrorTraceback(askLocationErr), - ), - ) - return &types.WrappedErrorMessage{Message: errorMessage, Err: askLocationErr} - } - - // Go back to the main screen - client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) - - return nil -} - // GetDiscoOrganizations gets the organizations list from the discovery server // If the list cannot be retrieved an error is returned. // If this is the case then a previous version of the list is returned if there is any. @@ -848,290 +203,6 @@ func (client *Client) GetDiscoServers() (*types.DiscoveryServers, error) { return servers, nil } -// 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 (client *Client) SetProfileID(profileID string) error { - errorMessage := "failed to set the profile ID for the current server" - server, serverErr := client.Servers.GetCurrentServer() - if serverErr != nil { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting a profile ID because no server configured, Err: %s", - types.GetErrorTraceback(serverErr), - ), - ) - client.goBackInternal() - return &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} - } - - base, baseErr := server.GetBase() - if baseErr != nil { - client.Logger.Error( - fmt.Sprintf("Failed setting a profile ID, Err: %s", types.GetErrorTraceback(serverErr)), - ) - client.goBackInternal() - return &types.WrappedErrorMessage{Message: errorMessage, Err: baseErr} - } - base.Profiles.Current = profileID - return nil -} - -// SetSearchServer sets the FSM to the SEARCH_SERVER state. -// This indicates that the user wants to search for a new server. -// Returns an error if this state transition is not possible. -func (client *Client) SetSearchServer() error { - if !client.FSM.HasTransition(STATE_SEARCH_SERVER) { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting search server, wrong state %s", - GetStateName(client.FSM.Current), - ), - ) - return &types.WrappedErrorMessage{ - Message: "failed to set search server", - Err: FSMWrongStateTransitionError{ - Got: client.FSM.Current, - Want: STATE_SEARCH_SERVER, - }.CustomError(), - } - } - - client.FSM.GoTransition(STATE_SEARCH_SERVER) - return nil -} - -// SetConnected sets the FSM to the CONNECTED state. -// This indicates that the VPN is connected to the server. -// Returns an error if this state transition is not possible. -func (client *Client) SetConnected() error { - errorMessage := "failed to set connected" - if client.InFSMState(STATE_CONNECTED) { - // already connected, show no error - client.Logger.Warning("Already connected") - return nil - } - if !client.FSM.HasTransition(STATE_CONNECTED) { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting connected, wrong state: %s", - GetStateName(client.FSM.Current), - ), - ) - return &types.WrappedErrorMessage{ - Message: errorMessage, - Err: FSMWrongStateTransitionError{ - Got: client.FSM.Current, - Want: STATE_CONNECTED, - }.CustomError(), - } - } - - currentServer, currentServerErr := client.Servers.GetCurrentServer() - if currentServerErr != nil { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting connected, cannot get current server with error: %s", - types.GetErrorTraceback(currentServerErr), - ), - ) - return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} - } - - client.FSM.GoTransitionWithData(STATE_CONNECTED, currentServer, false) - return nil -} - -// SetConnecting sets the FSM to the CONNECTING state. -// This indicates that the VPN is currently connecting to the server. -// Returns an error if this state transition is not possible. -func (client *Client) SetConnecting() error { - errorMessage := "failed to set connecting" - if client.InFSMState(STATE_CONNECTING) { - // already loading connection, show no error - client.Logger.Warning("Already connecting") - return nil - } - if !client.FSM.HasTransition(STATE_CONNECTING) { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting connecting, wrong state: %s", - GetStateName(client.FSM.Current), - ), - ) - return &types.WrappedErrorMessage{ - Message: errorMessage, - Err: FSMWrongStateTransitionError{ - Got: client.FSM.Current, - Want: STATE_CONNECTING, - }.CustomError(), - } - } - - currentServer, currentServerErr := client.Servers.GetCurrentServer() - if currentServerErr != nil { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting connecting, cannot get current server with error: %s", - types.GetErrorTraceback(currentServerErr), - ), - ) - return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} - } - - client.FSM.GoTransitionWithData(STATE_CONNECTING, currentServer, false) - return nil -} - -// SetDisconnecting sets the FSM to the DISCONNECTING state. -// This indicates that the VPN is currently disconnecting from the server. -// Returns an error if this state transition is not possible. -func (client *Client) SetDisconnecting() error { - errorMessage := "failed to set disconnecting" - if client.InFSMState(STATE_DISCONNECTING) { - // already disconnecting, show no error - client.Logger.Warning("Already disconnecting") - return nil - } - if !client.FSM.HasTransition(STATE_DISCONNECTING) { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting disconnecting, wrong state: %s", - GetStateName(client.FSM.Current), - ), - ) - return &types.WrappedErrorMessage{ - Message: errorMessage, - Err: FSMWrongStateTransitionError{ - Got: client.FSM.Current, - Want: STATE_DISCONNECTING, - }.CustomError(), - } - } - - currentServer, currentServerErr := client.Servers.GetCurrentServer() - if currentServerErr != nil { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting disconnected, cannot get current server with error: %s", - types.GetErrorTraceback(currentServerErr), - ), - ) - return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} - } - - client.FSM.GoTransitionWithData(STATE_DISCONNECTING, currentServer, false) - return nil -} - -// SetDisconnected sets the FSM to the DISCONNECTED state. -// This indicates that the VPN is currently disconnected from the server. -// This also sends the /disconnect API call to the server. -// Returns an error if this state transition is not possible. -func (client *Client) SetDisconnected(cleanup bool) error { - errorMessage := "failed to set disconnected" - if client.InFSMState(STATE_DISCONNECTED) { - // already disconnected, show no error - client.Logger.Warning("Already disconnected") - return nil - } - if !client.FSM.HasTransition(STATE_DISCONNECTED) { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting disconnected, wrong state: %s", - GetStateName(client.FSM.Current), - ), - ) - return &types.WrappedErrorMessage{ - Message: errorMessage, - Err: FSMWrongStateTransitionError{ - Got: client.FSM.Current, - Want: STATE_DISCONNECTED, - }.CustomError(), - } - } - - currentServer, currentServerErr := client.Servers.GetCurrentServer() - if currentServerErr != nil { - client.Logger.Warning( - fmt.Sprintf( - "Failed setting disconnect, failed getting current server with error: %s", - types.GetErrorTraceback(currentServerErr), - ), - ) - return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} - } - - if cleanup { - // Do the /disconnect API call and go to disconnected after... - server.Disconnect(currentServer) - } - - client.FSM.GoTransitionWithData(STATE_DISCONNECTED, currentServer, false) - - return nil -} - -// RenewSession renews the session for the current VPN server. -// This logs the user back in. -func (client *Client) RenewSession() error { - errorMessage := "failed to renew session" - - currentServer, currentServerErr := client.Servers.GetCurrentServer() - if currentServerErr != nil { - client.Logger.Warning( - fmt.Sprintf( - "Failed getting current server to renew, error: %s", - types.GetErrorTraceback(currentServerErr), - ), - ) - return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} - } - - server.MarkTokensForRenew(currentServer) - loginErr := client.ensureLogin(currentServer) - if loginErr != nil { - client.Logger.Warning( - fmt.Sprintf( - "Failed logging in server for renew, error: %s", - types.GetErrorTraceback(loginErr), - ), - ) - return &types.WrappedErrorMessage{Message: errorMessage, Err: loginErr} - } - - return nil -} - -// ShouldRenewButton returns true if the renew button should be shown -// If there is no server then this returns false and logs with INFO if so -// In other cases it simply checks the expiry time and calculates according to: https://github.com/eduvpn/documentation/blob/b93854dcdd22050d5f23e401619e0165cb8bc591/API.md#session-expiry. -func (client *Client) ShouldRenewButton() bool { - if !client.InFSMState(STATE_CONNECTED) && !client.InFSMState(STATE_CONNECTING) && - !client.InFSMState(STATE_DISCONNECTED) && - !client.InFSMState(STATE_DISCONNECTING) { - return false - } - - currentServer, currentServerErr := client.Servers.GetCurrentServer() - - if currentServerErr != nil { - client.Logger.Info( - fmt.Sprintf( - "No server found to renew with err: %s", - types.GetErrorTraceback(currentServerErr), - ), - ) - return false - } - - return server.ShouldRenewButton(currentServer) -} - -// InFSMState is a helper to check if the FSM is in state `checkState`. -func (client *Client) InFSMState(checkState FSMStateID) bool { - return client.FSM.InState(checkState) -} - // GetTranslated gets the translation for `languages` using the current state language. func (client *Client) GetTranslated(languages map[string]string) string { return util.GetLanguageMatched(languages, client.Language) diff --git a/client/fsm.go b/client/fsm.go index f8d2a1c..004a3cd 100644 --- a/client/fsm.go +++ b/client/fsm.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/eduvpn/eduvpn-common/internal/fsm" + "github.com/eduvpn/eduvpn-common/internal/server" "github.com/eduvpn/eduvpn-common/types" ) @@ -246,3 +247,264 @@ func (e FSMWrongStateError) CustomError() *types.WrappedErrorMessage { ), } } + + +// SetSearchServer sets the FSM to the SEARCH_SERVER state. +// This indicates that the user wants to search for a new server. +// Returns an error if this state transition is not possible. +func (client *Client) SetSearchServer() error { + if !client.FSM.HasTransition(STATE_SEARCH_SERVER) { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting search server, wrong state %s", + GetStateName(client.FSM.Current), + ), + ) + return &types.WrappedErrorMessage{ + Message: "failed to set search server", + Err: FSMWrongStateTransitionError{ + Got: client.FSM.Current, + Want: STATE_SEARCH_SERVER, + }.CustomError(), + } + } + + client.FSM.GoTransition(STATE_SEARCH_SERVER) + return nil +} + +// SetConnected sets the FSM to the CONNECTED state. +// This indicates that the VPN is connected to the server. +// Returns an error if this state transition is not possible. +func (client *Client) SetConnected() error { + errorMessage := "failed to set connected" + if client.InFSMState(STATE_CONNECTED) { + // already connected, show no error + client.Logger.Warning("Already connected") + return nil + } + if !client.FSM.HasTransition(STATE_CONNECTED) { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting connected, wrong state: %s", + GetStateName(client.FSM.Current), + ), + ) + return &types.WrappedErrorMessage{ + Message: errorMessage, + Err: FSMWrongStateTransitionError{ + Got: client.FSM.Current, + Want: STATE_CONNECTED, + }.CustomError(), + } + } + + currentServer, currentServerErr := client.Servers.GetCurrentServer() + if currentServerErr != nil { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting connected, cannot get current server with error: %s", + types.GetErrorTraceback(currentServerErr), + ), + ) + return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} + } + + client.FSM.GoTransitionWithData(STATE_CONNECTED, currentServer, false) + return nil +} + +// SetConnecting sets the FSM to the CONNECTING state. +// This indicates that the VPN is currently connecting to the server. +// Returns an error if this state transition is not possible. +func (client *Client) SetConnecting() error { + errorMessage := "failed to set connecting" + if client.InFSMState(STATE_CONNECTING) { + // already loading connection, show no error + client.Logger.Warning("Already connecting") + return nil + } + if !client.FSM.HasTransition(STATE_CONNECTING) { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting connecting, wrong state: %s", + GetStateName(client.FSM.Current), + ), + ) + return &types.WrappedErrorMessage{ + Message: errorMessage, + Err: FSMWrongStateTransitionError{ + Got: client.FSM.Current, + Want: STATE_CONNECTING, + }.CustomError(), + } + } + + currentServer, currentServerErr := client.Servers.GetCurrentServer() + if currentServerErr != nil { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting connecting, cannot get current server with error: %s", + types.GetErrorTraceback(currentServerErr), + ), + ) + return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} + } + + client.FSM.GoTransitionWithData(STATE_CONNECTING, currentServer, false) + return nil +} + +// SetDisconnecting sets the FSM to the DISCONNECTING state. +// This indicates that the VPN is currently disconnecting from the server. +// Returns an error if this state transition is not possible. +func (client *Client) SetDisconnecting() error { + errorMessage := "failed to set disconnecting" + if client.InFSMState(STATE_DISCONNECTING) { + // already disconnecting, show no error + client.Logger.Warning("Already disconnecting") + return nil + } + if !client.FSM.HasTransition(STATE_DISCONNECTING) { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting disconnecting, wrong state: %s", + GetStateName(client.FSM.Current), + ), + ) + return &types.WrappedErrorMessage{ + Message: errorMessage, + Err: FSMWrongStateTransitionError{ + Got: client.FSM.Current, + Want: STATE_DISCONNECTING, + }.CustomError(), + } + } + + currentServer, currentServerErr := client.Servers.GetCurrentServer() + if currentServerErr != nil { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting disconnected, cannot get current server with error: %s", + types.GetErrorTraceback(currentServerErr), + ), + ) + return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} + } + + client.FSM.GoTransitionWithData(STATE_DISCONNECTING, currentServer, false) + return nil +} + +// SetDisconnected sets the FSM to the DISCONNECTED state. +// This indicates that the VPN is currently disconnected from the server. +// This also sends the /disconnect API call to the server. +// Returns an error if this state transition is not possible. +func (client *Client) SetDisconnected(cleanup bool) error { + errorMessage := "failed to set disconnected" + if client.InFSMState(STATE_DISCONNECTED) { + // already disconnected, show no error + client.Logger.Warning("Already disconnected") + return nil + } + if !client.FSM.HasTransition(STATE_DISCONNECTED) { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting disconnected, wrong state: %s", + GetStateName(client.FSM.Current), + ), + ) + return &types.WrappedErrorMessage{ + Message: errorMessage, + Err: FSMWrongStateTransitionError{ + Got: client.FSM.Current, + Want: STATE_DISCONNECTED, + }.CustomError(), + } + } + + currentServer, currentServerErr := client.Servers.GetCurrentServer() + if currentServerErr != nil { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting disconnect, failed getting current server with error: %s", + types.GetErrorTraceback(currentServerErr), + ), + ) + return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} + } + + if cleanup { + // Do the /disconnect API call and go to disconnected after... + server.Disconnect(currentServer) + } + + client.FSM.GoTransitionWithData(STATE_DISCONNECTED, currentServer, false) + + return nil +} + +// goBackInternal uses the public go back but logs an error if it happened. +func (client *Client) goBackInternal() { + goBackErr := client.GoBack() + if goBackErr != nil { + client.Logger.Info( + fmt.Sprintf( + "Failed going back, error: %s", + types.GetErrorTraceback(goBackErr), + ), + ) + } +} + +// GoBack transitions the FSM back to the previous UI state, for now this is always the NO_SERVER state. +func (client *Client) GoBack() error { + errorMessage := "failed to go back" + if client.InFSMState(STATE_DEREGISTERED) { + client.Logger.Error("Wrong state, cannot go back when deregistered") + return &types.WrappedErrorMessage{ + Message: errorMessage, + Err: FSMDeregisteredError{}.CustomError(), + } + } + + // FIXME: Abitrary back transitions don't work because we need the approriate data + client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) + return nil +} + +// CancelOAuth cancels OAuth if one is in progress. +// If OAuth is not in progress, it returns an error. +// An error is also returned if OAuth is in progress but it fails to cancel it. +func (client *Client) CancelOAuth() error { + errorMessage := "failed to cancel OAuth" + if !client.InFSMState(STATE_OAUTH_STARTED) { + client.Logger.Error("Failed cancelling OAuth, not in the right state") + return &types.WrappedErrorMessage{ + Message: errorMessage, + Err: FSMWrongStateError{ + Got: client.FSM.Current, + Want: STATE_OAUTH_STARTED, + }.CustomError(), + } + } + + currentServer, serverErr := client.Servers.GetCurrentServer() + if serverErr != nil { + client.Logger.Warning( + fmt.Sprintf( + "Failed cancelling OAuth, no server configured to cancel OAuth for (err: %v)", + serverErr, + ), + ) + return &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} + } + server.CancelOAuth(currentServer) + return nil +} + + +// InFSMState is a helper to check if the FSM is in state `checkState`. +func (client *Client) InFSMState(checkState FSMStateID) bool { + return client.FSM.InState(checkState) +} diff --git a/client/server.go b/client/server.go new file mode 100644 index 0000000..468ff2e --- /dev/null +++ b/client/server.go @@ -0,0 +1,681 @@ +package client + +import ( + "errors" + "fmt" + + "github.com/eduvpn/eduvpn-common/types" + "github.com/eduvpn/eduvpn-common/internal/oauth" + "github.com/eduvpn/eduvpn-common/internal/server" + "github.com/eduvpn/eduvpn-common/internal/util" +) + +// getConfigAuth gets a config with authorization and authentication. +// It also asks for a profile if no valid profile is found. +func (client *Client) getConfigAuth( + chosenServer server.Server, + preferTCP bool, +) (string, string, error) { + loginErr := client.ensureLogin(chosenServer) + if loginErr != nil { + return "", "", loginErr + } + client.FSM.GoTransition(STATE_REQUEST_CONFIG) + + validProfile, profileErr := server.HasValidProfile(chosenServer) + if profileErr != nil { + return "", "", profileErr + } + + // No valid profile, ask for one + if !validProfile { + askProfileErr := client.askProfile(chosenServer) + if askProfileErr != nil { + return "", "", askProfileErr + } + } + + // We return the error otherwise we wrap it too much + return server.GetConfig(chosenServer, preferTCP) +} + +// 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 (client *Client) retryConfigAuth( + chosenServer server.Server, + preferTCP bool, +) (string, string, error) { + errorMessage := "failed authorized config retry" + config, configType, configErr := client.getConfigAuth(chosenServer, preferTCP) + if configErr != nil { + level := types.ERR_OTHER + var error *oauth.OAuthTokensInvalidError + var oauthCancelledError *oauth.OAuthCancelledCallbackError + + // Only retry if the error is that the tokens are invalid + if errors.As(configErr, &error) { + config, configType, configErr = client.getConfigAuth( + chosenServer, + preferTCP, + ) + if configErr == nil { + return config, configType, nil + } + } + if errors.As(configErr, &oauthCancelledError) { + level = types.ERR_INFO + } + client.goBackInternal() + return "", "", &types.WrappedErrorMessage{Level: level, Message: errorMessage, Err: configErr} + } + return config, configType, nil +} + +// 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 (client *Client) getConfig( + chosenServer server.Server, + preferTCP bool, +) (string, string, error) { + errorMessage := "failed to get a configuration for OpenVPN/Wireguard" + if client.InFSMState(STATE_DEREGISTERED) { + return "", "", &types.WrappedErrorMessage{ + Message: errorMessage, + Err: FSMDeregisteredError{}.CustomError(), + } + } + + config, configType, configErr := client.retryConfigAuth(chosenServer, preferTCP) + + if configErr != nil { + return "", "", &types.WrappedErrorMessage{Level: types.GetErrorLevel(configErr), Message: errorMessage, Err: configErr} + } + + currentServer, currentServerErr := client.Servers.GetCurrentServer() + if currentServerErr != nil { + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} + } + + // Signal the server display info + client.FSM.GoTransitionWithData(STATE_DISCONNECTED, currentServer, false) + + // Save the config + saveErr := client.Config.Save(&client) + if saveErr != nil { + client.Logger.Info( + fmt.Sprintf( + "Failed saving configuration after getting a server: %s", + types.GetErrorTraceback(saveErr), + ), + ) + } + + return config, configType, 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 (client *Client) SetSecureLocation(countryCode string) error { + errorMessage := "failed asking secure location" + + // Not supported with Let's Connect! + if client.isLetsConnect() { + return &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} + } + + server, serverErr := client.Discovery.GetServerByCountryCode(countryCode, "secure_internet") + if serverErr != nil { + client.Logger.Error( + fmt.Sprintf( + "Failed getting secure internet server by country code: %s with error: %s", + countryCode, + types.GetErrorTraceback(serverErr), + ), + ) + client.goBackInternal() + return &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} + } + + setLocationErr := client.Servers.SetSecureLocation(server) + if setLocationErr != nil { + client.Logger.Error( + fmt.Sprintf( + "Failed setting secure internet server with error: %s", + types.GetErrorTraceback(setLocationErr), + ), + ) + client.goBackInternal() + return &types.WrappedErrorMessage{Message: errorMessage, Err: setLocationErr} + } + return nil +} + +// 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 (client *Client) RemoveSecureInternet() error { + if client.InFSMState(STATE_DEREGISTERED) { + client.Logger.Error("Failed removing secure internet server due to deregistered") + return &types.WrappedErrorMessage{ + Message: "failed to remove Secure Internet", + Err: FSMDeregisteredError{}.CustomError(), + } + } + // No error because we can only have one secure internet server and if there are no secure internet servers, this is a NO-OP + client.Servers.RemoveSecureInternet() + client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) + // Save the config + saveErr := client.Config.Save(&client) + if saveErr != nil { + client.Logger.Info( + fmt.Sprintf( + "Failed saving configuration after removing a secure internet server: %s", + types.GetErrorTraceback(saveErr), + ), + ) + } + 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 (client *Client) RemoveInstituteAccess(url string) error { + if client.InFSMState(STATE_DEREGISTERED) { + return &types.WrappedErrorMessage{ + Message: "failed to remove Institute Access", + Err: FSMDeregisteredError{}.CustomError(), + } + } + // No error because this is a NO-OP if the server doesn't exist + client.Servers.RemoveInstituteAccess(url) + client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) + // Save the config + saveErr := client.Config.Save(&client) + if saveErr != nil { + client.Logger.Info( + fmt.Sprintf( + "Failed saving configuration after removing an institute access server: %s", + types.GetErrorTraceback(saveErr), + ), + ) + } + 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 (client *Client) RemoveCustomServer(url string) error { + if client.InFSMState(STATE_DEREGISTERED) { + return &types.WrappedErrorMessage{ + Message: "failed to remove Custom Server", + Err: FSMDeregisteredError{}.CustomError(), + } + } + // No error because this is a NO-OP if the server doesn't exist + client.Servers.RemoveCustomServer(url) + client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) + // Save the config + saveErr := client.Config.Save(&client) + if saveErr != nil { + client.Logger.Info( + fmt.Sprintf( + "Failed saving configuration after removing a custom server: %s", + types.GetErrorTraceback(saveErr), + ), + ) + } + return nil +} + +// AddInstituteServer adds an Institute Access server by `url`. +func (client *Client) AddInstituteServer(url string) (server.Server, error) { + errorMessage := fmt.Sprintf("failed adding Institute Access server with url %s", url) + + // Not supported with Let's Connect! + if client.isLetsConnect() { + return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} + } + + // Indicate that we're loading the server + client.FSM.GoTransition(STATE_LOADING_SERVER) + + // FIXME: Do nothing with discovery here as the client already has it + // So pass a server as the parameter + instituteServer, discoErr := client.Discovery.GetServerByURL(url, "institute_access") + if discoErr != nil { + client.goBackInternal() + return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: discoErr} + } + + // Add the secure internet server + server, serverErr := client.Servers.AddInstituteAccessServer(instituteServer) + if serverErr != nil { + client.goBackInternal() + return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} + } + + // Indicate that we want to authorize this server + client.FSM.GoTransition(STATE_CHOSEN_SERVER) + + // Authorize it + loginErr := client.ensureLogin(server) + if loginErr != nil { + // Removing is best effort + _ = client.RemoveInstituteAccess(url) + return nil, &types.WrappedErrorMessage{Level: types.GetErrorLevel(loginErr), Message: errorMessage, Err: loginErr} + } + + client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) + return server, 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 (client *Client) AddSecureInternetHomeServer(orgID string) (server.Server, error) { + errorMessage := fmt.Sprintf( + "failed adding Secure Internet home server with organization ID %s", + orgID, + ) + + // Not supported with Let's Connect! + if client.isLetsConnect() { + return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} + } + + // Indicate that we're loading the server + client.FSM.GoTransition(STATE_LOADING_SERVER) + + // Get the secure internet URL from discovery + secureOrg, secureServer, discoErr := client.Discovery.GetSecureHomeArgs(orgID) + if discoErr != nil { + client.goBackInternal() + return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: discoErr} + } + + // Add the secure internet server + server, serverErr := client.Servers.AddSecureInternet(secureOrg, secureServer) + if serverErr != nil { + client.goBackInternal() + return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} + } + + locationErr := client.askSecureLocation() + if locationErr != nil { + // Removing is best effort + _ = client.RemoveSecureInternet() + return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: locationErr} + } + + // Server has been chosen for authentication + client.FSM.GoTransition(STATE_CHOSEN_SERVER) + + // Authorize it + loginErr := client.ensureLogin(server) + if loginErr != nil { + // Removing is best effort + _ = client.RemoveSecureInternet() + return nil, &types.WrappedErrorMessage{Level: types.GetErrorLevel(loginErr), Message: errorMessage, Err: loginErr} + } + client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) + return server, nil +} + +// AddCustomServer adds a Custom Server by `url` +func (client *Client) AddCustomServer(url string) (server.Server, error) { + errorMessage := fmt.Sprintf("failed adding Custom server with url %s", url) + + url, urlErr := util.EnsureValidURL(url) + if urlErr != nil { + return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: urlErr} + } + + // Indicate that we're loading the server + client.FSM.GoTransition(STATE_LOADING_SERVER) + + customServer := &types.DiscoveryServer{ + BaseURL: url, + DisplayName: map[string]string{"en": url}, + Type: "custom_server", + } + + // A custom server is just an institute access server under the hood + server, serverErr := client.Servers.AddCustomServer(customServer) + if serverErr != nil { + client.goBackInternal() + return nil, &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} + } + + // Server has been chosen for authentication + client.FSM.GoTransition(STATE_CHOSEN_SERVER) + + // Authorize it + loginErr := client.ensureLogin(server) + if loginErr != nil { + // removing is best effort + _ = client.RemoveCustomServer(url) + return nil, &types.WrappedErrorMessage{Level: types.GetErrorLevel(loginErr), Message: errorMessage, Err: loginErr} + } + + client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) + return server, 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 (client *Client) GetConfigInstituteAccess(url string, preferTCP bool) (string, string, error) { + errorMessage := fmt.Sprintf("failed getting a configuration for Institute Access %s", url) + + // Not supported with Let's Connect! + if client.isLetsConnect() { + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} + } + + client.FSM.GoTransition(STATE_LOADING_SERVER) + + // Get the server if it exists + server, serverErr := client.Servers.GetInstituteAccess(url) + if serverErr != nil { + client.Logger.Error( + fmt.Sprintf( + "Failed getting an institute access server configuration with error: %s", + types.GetErrorTraceback(serverErr), + ), + ) + client.goBackInternal() + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} + } + + // Set the server as the current + currentErr := client.Servers.SetInstituteAccess(server) + if currentErr != nil { + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: currentErr} + } + + // The server has now been chosen + client.FSM.GoTransition(STATE_CHOSEN_SERVER) + + config, configType, configErr := client.getConfig(server, preferTCP) + if configErr != nil { + client.Logger.Inherit(configErr, + fmt.Sprintf( + "Failed getting an institute access server configuration with error: %s", + types.GetErrorTraceback(configErr), + ), + ) + client.goBackInternal() + return "", "", &types.WrappedErrorMessage{Level: types.GetErrorLevel(configErr), Message: errorMessage, Err: configErr} + } + return config, configType, nil +} + +// 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. +func (client *Client) GetConfigSecureInternet( + orgID string, + preferTCP bool, +) (string, string, error) { + errorMessage := fmt.Sprintf( + "failed getting a configuration for Secure Internet organization %s", + orgID, + ) + + // Not supported with Let's Connect! + if client.isLetsConnect() { + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: LetsConnectNotSupportedError{}} + } + + client.FSM.GoTransition(STATE_LOADING_SERVER) + + // Get the server if it exists + server, serverErr := client.Servers.GetSecureInternetHomeServer() + if serverErr != nil { + client.Logger.Error( + fmt.Sprintf( + "Failed getting a custom server configuration with error: %s", + types.GetErrorTraceback(serverErr), + ), + ) + client.goBackInternal() + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} + } + + // Set the server as the current + currentErr := client.Servers.SetSecureInternet(server) + if currentErr != nil { + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: currentErr} + } + + client.FSM.GoTransition(STATE_CHOSEN_SERVER) + + config, configType, configErr := client.getConfig(server, preferTCP) + if configErr != nil { + client.Logger.Inherit( + configErr, + fmt.Sprintf( + "Failed getting a secure internet configuration with error: %s", + types.GetErrorTraceback(configErr), + ), + ) + client.goBackInternal() + return "", "", &types.WrappedErrorMessage{Level: types.GetErrorLevel(configErr), Message: errorMessage, Err: configErr} + } + return config, configType, nil +} + +// 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 (client *Client) GetConfigCustomServer(url string, preferTCP bool) (string, string, error) { + errorMessage := fmt.Sprintf("failed getting a configuration for custom server %s", url) + + url, urlErr := util.EnsureValidURL(url) + if urlErr != nil { + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: urlErr} + } + + client.FSM.GoTransition(STATE_LOADING_SERVER) + + // Get the server if it exists + server, serverErr := client.Servers.GetCustomServer(url) + if serverErr != nil { + client.Logger.Error( + fmt.Sprintf( + "Failed getting a custom server configuration with error: %s", + types.GetErrorTraceback(serverErr), + ), + ) + client.goBackInternal() + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} + } + + // Set the server as the current + currentErr := client.Servers.SetCustomServer(server) + if currentErr != nil { + return "", "", &types.WrappedErrorMessage{Message: errorMessage, Err: currentErr} + } + + client.FSM.GoTransition(STATE_CHOSEN_SERVER) + + config, configType, configErr := client.getConfig(server, preferTCP) + if configErr != nil { + client.Logger.Inherit( + configErr, + fmt.Sprintf( + "Failed getting a custom server with error: %s", + types.GetErrorTraceback(configErr), + ), + ) + client.goBackInternal() + return "", "", &types.WrappedErrorMessage{Level: types.GetErrorLevel(configErr), Message: errorMessage, Err: configErr} + } + return config, configType, nil +} + +// askSecureLocation asks the user to choose a Secure Internet location by moving the FSM to the STATE_ASK_LOCATION state. +func (client *Client) askSecureLocation() error { + locations := client.Discovery.GetSecureLocationList() + + // Ask for the location in the callback + client.FSM.GoTransitionWithData(STATE_ASK_LOCATION, locations, false) + + // The state has changed, meaning setting the secure location was not successful + if client.FSM.Current != STATE_ASK_LOCATION { + // TODO: maybe a custom type for this errors.new? + return &types.WrappedErrorMessage{ + Message: "failed setting secure location", + Err: errors.New("failed loading secure location"), + } + } + return nil +} + +// ChangeSecureLocation changes the location for an existing Secure Internet Server. +// Changing a secure internet location is only possible when the user is in the main screen (STATE_NO_SERVER), otherwise it returns an error. +// It also returns an error if something has gone wrong when selecting the new location +func (client *Client) ChangeSecureLocation() error { + errorMessage := "failed to change location from the main screen" + + if !client.InFSMState(STATE_NO_SERVER) { + client.Logger.Error("Failed changing secure internet location, not in the right state") + return &types.WrappedErrorMessage{ + Message: errorMessage, + Err: FSMWrongStateError{ + Got: client.FSM.Current, + Want: STATE_NO_SERVER, + }.CustomError(), + } + } + + askLocationErr := client.askSecureLocation() + if askLocationErr != nil { + client.Logger.Error( + fmt.Sprintf( + "Failed changing secure internet location, err: %s", + types.GetErrorTraceback(askLocationErr), + ), + ) + return &types.WrappedErrorMessage{Message: errorMessage, Err: askLocationErr} + } + + // Go back to the main screen + client.FSM.GoTransitionWithData(STATE_NO_SERVER, client.Servers, false) + + return nil +} + + +// RenewSession renews the session for the current VPN server. +// This logs the user back in. +func (client *Client) RenewSession() error { + errorMessage := "failed to renew session" + + currentServer, currentServerErr := client.Servers.GetCurrentServer() + if currentServerErr != nil { + client.Logger.Warning( + fmt.Sprintf( + "Failed getting current server to renew, error: %s", + types.GetErrorTraceback(currentServerErr), + ), + ) + return &types.WrappedErrorMessage{Message: errorMessage, Err: currentServerErr} + } + + server.MarkTokensForRenew(currentServer) + loginErr := client.ensureLogin(currentServer) + if loginErr != nil { + client.Logger.Warning( + fmt.Sprintf( + "Failed logging in server for renew, error: %s", + types.GetErrorTraceback(loginErr), + ), + ) + return &types.WrappedErrorMessage{Message: errorMessage, Err: loginErr} + } + + return nil +} + +// ShouldRenewButton returns true if the renew button should be shown +// If there is no server then this returns false and logs with INFO if so +// In other cases it simply checks the expiry time and calculates according to: https://github.com/eduvpn/documentation/blob/b93854dcdd22050d5f23e401619e0165cb8bc591/API.md#session-expiry. +func (client *Client) ShouldRenewButton() bool { + if !client.InFSMState(STATE_CONNECTED) && !client.InFSMState(STATE_CONNECTING) && + !client.InFSMState(STATE_DISCONNECTED) && + !client.InFSMState(STATE_DISCONNECTING) { + return false + } + + currentServer, currentServerErr := client.Servers.GetCurrentServer() + + if currentServerErr != nil { + client.Logger.Info( + fmt.Sprintf( + "No server found to renew with err: %s", + types.GetErrorTraceback(currentServerErr), + ), + ) + return false + } + + return server.ShouldRenewButton(currentServer) +} + +// ensureLogin logs the user back in if needed. +// It runs the FSM transitions to ask for user input. +func (client *Client) ensureLogin(chosenServer server.Server) error { + errorMessage := "failed ensuring login" + // Relogin with oauth + // This moves the state to authorized + if server.NeedsRelogin(chosenServer) { + url, urlErr := server.GetOAuthURL(chosenServer, client.Name) + + client.FSM.GoTransitionWithData(STATE_OAUTH_STARTED, url, true) + + if urlErr != nil { + client.goBackInternal() + return &types.WrappedErrorMessage{Message: errorMessage, Err: urlErr} + } + + exchangeErr := server.OAuthExchange(chosenServer) + + if exchangeErr != nil { + client.goBackInternal() + return &types.WrappedErrorMessage{Message: errorMessage, Err: exchangeErr} + } + } + // OAuth was valid, ensure we are in the authorized state + client.FSM.GoTransition(STATE_AUTHORIZED) + return nil +} + +// 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 (client *Client) SetProfileID(profileID string) error { + errorMessage := "failed to set the profile ID for the current server" + server, serverErr := client.Servers.GetCurrentServer() + if serverErr != nil { + client.Logger.Warning( + fmt.Sprintf( + "Failed setting a profile ID because no server configured, Err: %s", + types.GetErrorTraceback(serverErr), + ), + ) + client.goBackInternal() + return &types.WrappedErrorMessage{Message: errorMessage, Err: serverErr} + } + + base, baseErr := server.GetBase() + if baseErr != nil { + client.Logger.Error( + fmt.Sprintf("Failed setting a profile ID, Err: %s", types.GetErrorTraceback(serverErr)), + ) + client.goBackInternal() + return &types.WrappedErrorMessage{Message: errorMessage, Err: baseErr} + } + base.Profiles.Current = profileID + return nil +} + |
