diff options
| author | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-05-13 12:12:22 +0200 |
|---|---|---|
| committer | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-05-13 12:12:22 +0200 |
| commit | 5abf00ab87a55662eefc7716de52ead9749293c6 (patch) | |
| tree | 1cfa64b99482d7cc08b1d7da5d6833b75f5f7714 | |
| parent | 57d6c2ac55a5fd1ea609c873d5410174b7cf6ca4 (diff) | |
Refactor: Adapt the API to the documentation
| -rw-r--r-- | cmd/cli/main.go | 26 | ||||
| -rw-r--r-- | docs/src/about.md | 11 | ||||
| -rw-r--r-- | docs/src/api/go/example.md | 72 | ||||
| -rw-r--r-- | docs/src/api/go/functions.md | 11 | ||||
| -rw-r--r-- | docs/src/api/overview/README.md | 2 | ||||
| -rw-r--r-- | docs/src/api/overview/getconfig.md | 9 | ||||
| -rw-r--r-- | docs/src/api/python/example.md | 49 | ||||
| -rw-r--r-- | docs/src/api/python/functions.md | 49 | ||||
| -rw-r--r-- | docs/src/gettingstarted/README.md | 2 | ||||
| -rw-r--r-- | docs/src/gettingstarted/building/python.md | 2 | ||||
| -rw-r--r-- | docs/src/gettingstarted/debugging/fsm.md | 8 | ||||
| -rw-r--r-- | docs/src/gettingstarted/debugging/fsm_example.svg | 2 | ||||
| -rw-r--r-- | docs/src/godifferences.png | bin | 0 -> 73985 bytes | |||
| -rw-r--r-- | exports/exports.go | 18 | ||||
| -rw-r--r-- | internal/openvpn.go | 8 | ||||
| -rw-r--r-- | internal/server.go | 35 | ||||
| -rw-r--r-- | internal/wireguard.go | 12 | ||||
| -rw-r--r-- | state.go | 21 | ||||
| -rw-r--r-- | state_test.go | 14 | ||||
| -rw-r--r-- | wrappers/python/main.py | 92 | ||||
| -rw-r--r-- | wrappers/python/src/__init__.py | 36 | ||||
| -rw-r--r-- | wrappers/python/src/main.py | 189 | ||||
| -rw-r--r-- | wrappers/python/tests.py | 29 |
23 files changed, 414 insertions, 283 deletions
diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 5a35c8e..f46d156 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -13,6 +13,7 @@ import ( eduvpn "github.com/jwijenbergh/eduvpn-common" ) +// Open a browser with xdg-open func openBrowser(urlString string) { fmt.Printf("OAuth: Initialized with AuthURL %s\n", urlString) fmt.Println("OAuth: Opening browser with xdg-open...") @@ -20,13 +21,13 @@ func openBrowser(urlString string) { } // Taken from internal/server.go as it's an internal API for now +// These are used to parse the profile info type ServerProfile struct { ID string `json:"profile_id"` DisplayName string `json:"display_name"` VPNProtoList []string `json:"vpn_proto_list"` DefaultGateway bool `json:"default_gateway"` } - type ServerProfileInfo struct { Current string `json:"current_profile"` Info struct { @@ -34,6 +35,7 @@ type ServerProfileInfo struct { } `json:"info"` } +// Ask for a profile in the command line func sendProfile(state *eduvpn.VPNState, data string) { fmt.Printf("Multiple VPN profiles found. Please select a profile by entering e.g. 1") serverProfiles := &ServerProfileInfo{} @@ -72,6 +74,9 @@ func sendProfile(state *eduvpn.VPNState, data string) { } } +// The callback function +// If OAuth is started we open the browser with the Auth URL +// If we ask for a profile, we send the profile using command line input func stateCallback(state *eduvpn.VPNState, oldState string, newState string, data string) { if newState == "OAuth_Started" { openBrowser(data) @@ -82,24 +87,28 @@ func stateCallback(state *eduvpn.VPNState, oldState string, newState string, dat } } -func getConfig(state *eduvpn.VPNState, url string, isInstitute bool) (string, error) { +// Get a config for Institute Access or Secure Internet Server +func getConfig(state *eduvpn.VPNState, url string, isInstitute bool) (string, string, error) { if !strings.HasPrefix(url, "https://") { url = "https://" + url } if !strings.HasSuffix(url, "/") { url += "/" } + // Force TCP is set to False if isInstitute { return state.GetConfigInstituteAccess(url, false) } return state.GetConfigSecureInternet(url, false) } +// A discovery entry for a server type ServerDiscoEntry struct { ServerType string `json:"server_type"` BaseURL string `json:"base_url"` } +// Gets all different Secure Internet server by parsing the JSON from the discovery func getAllSecureInternetServers(serverList string) ([]string, error) { var secureInternet []string @@ -120,12 +129,13 @@ func getAllSecureInternetServers(serverList string) ([]string, error) { return secureInternet, nil } +// Store the Secure Internet config in a certificate folder func storeSecureInternetConfig(state *eduvpn.VPNState, url string, directory string) { os.MkdirAll(directory, os.ModePerm) fmt.Println("Creating and storing cert for", url) - config, configErr := getConfig(state, url, false) + config, _, configErr := getConfig(state, url, false) if configErr != nil { fmt.Printf("Failed obtaining config for url %s with error %v\n", url, configErr) @@ -140,12 +150,13 @@ func storeSecureInternetConfig(state *eduvpn.VPNState, url string, directory str } } +// This is basically used to get a certificate for all Secure Internet servers func getSecureInternetAll(homeURL string) { state := &eduvpn.VPNState{} state.Register("org.eduvpn.app.linux", "configs", func(old string, new string, data string) { stateCallback(state, old, new, data) - }, false) + }, true) defer state.Deregister() @@ -180,16 +191,17 @@ func getSecureInternetAll(homeURL string) { fmt.Println("Done storing all certs in directory:", directory) } +// Get a config for a single server, Institute Access or Secure Internet func printConfig(url string, isInstitute bool) { state := &eduvpn.VPNState{} state.Register("org.eduvpn.app.linux", "configs", func(old string, new string, data string) { stateCallback(state, old, new, data) - }, false) + }, true) defer state.Deregister() - config, configErr := getConfig(state, url, isInstitute) + config, _, configErr := getConfig(state, url, isInstitute) if configErr != nil { fmt.Println("Error getting config", configErr) @@ -199,6 +211,8 @@ func printConfig(url string, isInstitute bool) { fmt.Println("Obtained config", config) } +// The main function +// It parses the arguments and executes the correct functions func main() { urlArg := flag.String("get-institute", "", "The url of an institute to connect to") secureInternet := flag.String("get-secure", "", "Gets secure internet servers.") diff --git a/docs/src/about.md b/docs/src/about.md index e2715c0..672ae6a 100644 --- a/docs/src/about.md +++ b/docs/src/about.md @@ -1,8 +1,8 @@ # About This chapter contains background information for the library. We give a general introduction to eduVPN and explain what problems this library aims to solve. -## eduVPN introduction -eduVPN-common is a library for [eduVPN](https://www.eduvpn.org/), which is a VPN by [Surf](https://www.surf.nl) for research institutions such as Universities. Each institution that uses eduVPN has its own server. To discover these servers and establish a VPN connection with them, eduVPN clients are used. eduVPN has clients for each common platform: +## EduVPN introduction +eduVPN-common is a library for [eduVPN](https://www.eduvpn.org/), which is a VPN by [SURF](https://www.surf.nl) and a project by [GÉANT](https://geant.org/), for research institutes such as Universities. Each institute that uses eduVPN has its own server. To discover these servers and establish a VPN connection with them, eduVPN clients are used. eduVPN has clients for each common platform: - [Android](https://github.com/eduvpn/android) - [Linux](https://github.com/eduvpn/python-eduvpn-client) - [MacOS/iOS](https://github.com/eduvpn/apple) @@ -12,8 +12,13 @@ eduVPN-common is a library for [eduVPN](https://www.eduvpn.org/), which is a VPN However, as these clients are rather similar in functionality, apart from platform specific differences, right now there is duplicate code between them. For example, the process to discover institution's servers, the authorization process (OAuth) and Wireguard key generation. This goal of this library is to provide the common functionality between these clients into one codebase. The library is written in the [Go](https://go.dev/) language and has wrapper code for each of the languages that are used by the current clients. +The main goal is thus the following: + + +This library tries to remove non-platform specific common functionality. This way eduVPN clients have less duplicate code. + ## License [MIT](https://github.com/jwijenbergh/eduvpn-common/blob/main/LICENSE) ## Authors -This library is written by [Steven Wallis de Vries](https://github.com/stevenwdv) and [Jeroen Wijenbergh](https://github.com/jwijenbergh), two Radboud University students that worked at Surf for their research internship. +This library is written by [Steven Wallis de Vries](https://github.com/stevenwdv) and [Jeroen Wijenbergh](https://github.com/jwijenbergh), two Radboud University students that worked at SURF for their research internship. diff --git a/docs/src/api/go/example.md b/docs/src/api/go/example.md index e7b0d36..0ed7d02 100644 --- a/docs/src/api/go/example.md +++ b/docs/src/api/go/example.md @@ -1,63 +1,13 @@ -# Example with Comments - +# Example with comments +The following is an example [in the repository](https://github.com/jwijenbergh/eduvpn-common/blob/main/cmd/cli/main.go). It is a command line client with the following flags +``` +-get-institute string + The url of an institute to connect to +-get-secure string + Gets secure internet servers. +-get-secure-all string + Gets certificates for all secure internet servers. It stores them in ./certs. Provide an URL for the home server e.g. nl.eduvpn.org. +``` ```go - -// Bring the library into scope with the eduvpn prefix -import eduvpn "github.com/jwijenbergh/eduvpn-common" - -// Callbacks - -func stateCallback(state *eduvpn.VPNState, oldState string, newState string, data string) { - - // OAuth is started, open the browser with the authorization URL - if newState == "OAuth_Started" { - openBrowser(data) - } - - // Multiple profiles are found, we need to send a profile ID back using state.SetProfileID - if newState == "Ask_Profile" { - selectAndSendProfile(state, data) - } -} - -func main() { - // Create the VPNState - state := &eduvpn.VPNState{} - - // Register the state - // We use linux so the client ID will be org.eduvpn.app.linux - // We want to store the config files in configs - // We wrap the callback with the state argument - // And enable debugging - registerErr := state.Register("org.eduvpn.app.linux", "configs", func(old string, new string, data string) { - stateCallback(state, old, new, data) - }, true) - - if registErr != nil { - // handle the error of not being able to register - } - - // Cleanup the library at the end - defer state.Deregister() - - // Connect to an example server without forcing TCP - config, configType, configErr := state.GetConnectConfig("eduvpn.example.com", false) - - if configErr != nil { - // handle the error of not being able to get a config - } - - if configType == "wireguard" { - // Connect using wireguard with the config - } else { - // Connect using OpenVPN with the config - } - - // We are connected - setConnectErr := state.SetConnected() - - if setConnectErr != nil { - // handle the error of not being able to call set connected - } -} +{{#include ../../../../cmd/cli/main.go}} ``` diff --git a/docs/src/api/go/functions.md b/docs/src/api/go/functions.md index c647787..381f5be 100644 --- a/docs/src/api/go/functions.md +++ b/docs/src/api/go/functions.md @@ -26,9 +26,10 @@ Returns a string of JSON data with the servers/organizations and an `error`, nil ## OpenVPN/Wireguard config See [Overview](../overview/getconfig.html) ```go -func GetConnectConfig(url string, forceTCP bool) (string, string, error) +func GetConfigInstituteAccess(url string, forceTCP bool) (string, string, error) +func GetConfigSecureInternet(url string, forceTCP bool) (string, string, error) ``` -- `url`: The url of the server to get a connect config for +- `url`: The URL of the Institute Access or Secure Internet server to get a connect config for - `forceTCP`: Whether or not we want to force enable TCP Returns: @@ -36,6 +37,12 @@ Returns: - A `string`, `openvpn` or `wireguard` indicating if it is an OpenVPN or Wireguard config - An `error` (can be nil) +### Cancelling OAuth +```go +func CancelOAuth() error +``` +Returns an `error`, can be nil indicating no error + ### Setting a profile ID ```go func SetProfileID(profileID string) error diff --git a/docs/src/api/overview/README.md b/docs/src/api/overview/README.md index d233b0c..8af056a 100644 --- a/docs/src/api/overview/README.md +++ b/docs/src/api/overview/README.md @@ -1,4 +1,4 @@ -# API Overview +# API overview This section defines the API in high-level, we explain what functions there are, what their use is and what a typical flow is for creating an eduVPN client with this library. The language specific documentation will be given in separate sections. diff --git a/docs/src/api/overview/getconfig.md b/docs/src/api/overview/getconfig.md index 70bcb39..ca35835 100644 --- a/docs/src/api/overview/getconfig.md +++ b/docs/src/api/overview/getconfig.md @@ -1,6 +1,6 @@ # Getting an OpenVPN/Wireguard config ## Summary -name: `Get Connect Config` +name: `Get Config Institute Access` and `Get Config Secure Internet` | Arguments | Description | type | | --------- | -------------------------------------- | -------- | @@ -13,9 +13,10 @@ Used to obtain the OpenVPN/Wireguard config ## Detailed information -To get a configuration that is used to actually establish a tunnel with the VPN server, we have the Get Connect Config (the exact name depends on the language you're using) function in the library. This function has two parameters *URL* and *Force TCP*. +To get a configuration that is used to actually establish a tunnel with the VPN server, we have the Get Config function for Institute Access and Secure Internet (the exact name depends on the language you're using) function in the library. This function has two parameters *URL* and *Force TCP*. -*URL* is the base url of the server to connect to e.g. `nl.eduvpn.org`. Note that this function does not need any further input whether or not we want to connect for institute access or secure internet. This is handled in the library by checking the discovery list. If it does not find the server in the discovery list, it assumes it wants to connect as an institute server. +*URL* is the base url of the server to connect to +e.g. `nl.eduvpn.org`. Use the correct function to indicate if it is an Institute Access server or a Secure Internet server. A user configured server is often an Institute Access server. The *Force TCP* flag is a boolean that indicates whether or not we want to use TCP to connect over the VPN. This flag is useful if the user has enabled e.g. a setting that forces the use of TCP, which is only supported by OpenVPN. If the Force TCP flag is set to true but the server only supports Wireguard then an error is returned and the config will be empty. @@ -33,6 +34,8 @@ The format of the authorization URL is e.g. this: `https://eduvpn.example.com/vpn-user-portal/oauth/authorize?client_id=org.eduvpn.app.linux&code_challenge=DsmGyWFBkvDXiIO33Fs40Z0fn4pxtzDCW2jKvAMptBg&code_challenge_method=S256&redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2Fcallback&response_type=code&scope=config&state=vha2Krx-HpOyvFkWsWYmey0jrHQ6bnb06PQ6zBXX_bg` +This callback can be cancelled by using a `Cancel OAuth` function. + ### Callback: Selecting a profile Another aspect that needs to be taken into account is the fact that there can be multiple profiles that a client can connect to. When the function gets called for obtaining an OpenVPN/Wireguard configuration, it asks the client which profile it wants to connect to using the callback that gets triggered on the Ask Profile state. The data is the list of profiles in JSON format, e.g. diff --git a/docs/src/api/python/example.md b/docs/src/api/python/example.md index 7c0997f..f371b68 100644 --- a/docs/src/api/python/example.md +++ b/docs/src/api/python/example.md @@ -1,48 +1,7 @@ -# Example with Comments +# Example with comments -```python -import eduvpncommon.main as eduvpn - -# Callbacks -@_eduvpn.event.on("OAuth_Started", eduvpn.StateType.Enter) -def oauth_initialized(url): - # Open the webbrowser with the url - webbrowser.open(url) - - -@_eduvpn.event.on("Ask_Profile", eduvpn.StateType.Enter) -def ask_profile(profiles): - # Set a profile - _eduvpn.set_profile("example") - -# Register the state -# We use linux so the client ID will be org.eduvpn.app.linux -# We want to store the config files in configs -# And enable debugging -_eduvpn = eduvpn.EduVPN("org.eduvpn.app.linux", "configs") -register_err = _eduvpn.register(debug=True) - -if register_err: - # Handle error +This is an example that can also be found [in the repository](https://github.com/jwijenbergh/eduvpn-common/blob/main/wrappers/python/main.py). It gets a config from an Institute Access server with support for multiple profiles. -# Connect to eduvpn.example.com -config, config_type, config_err = _eduvpn.get_connect_config("eduvpn.example.com", False) - -if config_err: - # Handle error - -if config_type == "wireguard": - # Connect using wireguard with the config -elif config_type == "openvpn": - # Connect using OpenVPN with the config -else: - # Handle error - -# Set connected -set_connect_err = _eduvpn.set_connected() -if set_connect_err: - # Handle error - -# Handle cleanup -_eduvpn.deregister() +```python +{{#include ../../../../wrappers/python/main.py}} ``` diff --git a/docs/src/api/python/functions.md b/docs/src/api/python/functions.md index ebfb774..2a348cd 100644 --- a/docs/src/api/python/functions.md +++ b/docs/src/api/python/functions.md @@ -12,59 +12,70 @@ def __init__(self, name: str, directory: str) ## Registering See [Overview](../overview/registering.html) ```python -def register(self, debug=False: bool) -> Optional[str] +def register(self, debug: bool = False) -> None ``` -- `debug`: Whether or not we want to enable debugging +- `debug`: Whether or not we want to enable debugging, default: `False` -Returns an optional `string` for the error message +Returns nothing. Raises an exception in case of an error. ## Discovery See [Overview](../overview/discovery.html) ```python -def get_disco_servers(self) -> (Optional[str], Optional[str]) +def get_disco_servers(self) -> str ``` ```python -def get_disco_organizations(self) -> (Optional[str], Optional[str]) +def get_disco_organizations(self) -> str ``` -Returns an optional `string` of JSON data with the servers/organizations and an optional error message +Returns a `string` of JSON data with the servers/organizations. Raises an exception in case of an error. ## OpenVPN/Wireguard config See [Overview](../overview/getconfig.html) ```python -def get_connect_config(self, url: str, forceTCP: bool) -> (Optional[str], Optional[str], Optional[str]) +def get_config_institute_access(self, url: str, forceTCP: bool = False) -> Tuple[str, str] ``` -- `url`: The url of the server to get a connect config for -- `forceTCP`: Whether or not we want to force enable TCP +```python +def get_config_secure_internet(self, url: str, forceTCP: bool = False) -> Tuple[str, str] +``` +- `url`: The url of the Secure Internet or Institute Access server to get a connect config for +- `forceTCP`: Whether or not we want to force enable TCP, default: `False` Returns: -- An optional `string` of the OpenVPN/Wireguard config -- An optional `string`, `openvpn` or `wireguard` indicating if it is an OpenVPN or Wireguard config -- An optional error message `string` +- A `string` of the OpenVPN/Wireguard config +- An `string`, `openvpn` or `wireguard` indicating if it is an OpenVPN or Wireguard config + +Raises an exception in case of an error. + +### Cancelling OAuth +```python +def cancel_oauth(self) -> None +``` + +Returns nothing. Raises an exception in case of an error. ### Setting a profile ID ```python -def set_profile(self, profile_id: str) -> Optional[str] +def set_profile(self, profile_id: str) -> None ``` - `profile_id`: The profile ID to connect to -Returns an optional `string`, which is the error message +Returns nothing. Raises an exception in case of an errorr. ## Connecting/Disconnecting See [Overview](../overview/connecting.html) ```python -def set_connected(self) -> Optional[str] +def set_connected(self) -> None ``` ```python -def set_disconnected(self) -> Optional[str] +def set_disconnected(self) -> None ``` -Returns an optional `string`, which is the error message +Returns an nothing. Raises an exception in case of an error. ## Deregister See [Overview](../overview/deregistering.html) ```python -def deregister() -> Optional[str] +def deregister() -> None ``` -Returns an optional `string`, which is the error message +Returns nothing. Raises an exception in case of an error. diff --git a/docs/src/gettingstarted/README.md b/docs/src/gettingstarted/README.md index 7c7912d..d2c7455 100644 --- a/docs/src/gettingstarted/README.md +++ b/docs/src/gettingstarted/README.md @@ -1,4 +1,4 @@ -# Getting Started +# Getting started This chapter contains the steps to get started with the Go library. You will learn how to build the library, how to run the test suite and how to debug possible errors. The documentation on how to use this library will be given in the next chapter: [API](../api/index.html). diff --git a/docs/src/gettingstarted/building/python.md b/docs/src/gettingstarted/building/python.md index 6995f9e..78ce424 100644 --- a/docs/src/gettingstarted/building/python.md +++ b/docs/src/gettingstarted/building/python.md @@ -1,4 +1,4 @@ -# Python Wrapper +# Python wrapper To build the python wrapper issue the following command (in the root directory of the eduvpn-common project): diff --git a/docs/src/gettingstarted/debugging/fsm.md b/docs/src/gettingstarted/debugging/fsm.md index 11d51de..8479a92 100644 --- a/docs/src/gettingstarted/debugging/fsm.md +++ b/docs/src/gettingstarted/debugging/fsm.md @@ -1,6 +1,6 @@ -# Finite State Machine +# Finite state machine -The eduvpn-common library uses a Finite State Machine internally to keep track of which state the client is in and to communicate data callbacks (e.g. to communicate the Authorization URL in the OAuth process to the client). +The eduvpn-common library uses a finite state machine internally to keep track of which state the client is in and to communicate data callbacks (e.g. to communicate the Authorization URL in the OAuth process to the client). ## Viewing the FSM To view the FSM in an image, set the debug variable to `True`. This outputs the graph with a `.graph` extension in the client-specified config directory (See [API](../../api/index.html)). The format of this graph is from [Mermaid](https://mermaid-js.github.io/mermaid/#/). @@ -9,7 +9,7 @@ If you have the [Mermaid command line client](https://github.com/mermaid-js/merm If you do not want to install additional tools to view the graph, you can submit the contents of the `.graph` file to the [Mermaid Live Editor](https://mermaid.live/) to see the image. -## FSM Example +## FSM example The following is an example of the FSM when the client has obtained a Wireguard/OpenVPN configuration from an eduVPN server  @@ -21,7 +21,7 @@ The states mean the following: - `Deregistered`: The client has not registered with the library yet, the state variables are not initialized - `No_Server`: The client is registered, but has not chosen a server yet -- `Chosen_ServeR`: The client has chosen a server to connect to +- `Chosen_Server`: The client has chosen a server to connect to - `OAuth_Started`: The OAuth process has been started. This means that the client needs to redirect to the browser so that the user can login and approve the application - `Authorized`: The OAuth process has finished. The client now has tokens and is thus authorized - `Request_Config`: The client is in the process of requesting an OpenVPN/Wireguard configuration from the server diff --git a/docs/src/gettingstarted/debugging/fsm_example.svg b/docs/src/gettingstarted/debugging/fsm_example.svg index 9b19280..134b0fb 100644 --- a/docs/src/gettingstarted/debugging/fsm_example.svg +++ b/docs/src/gettingstarted/debugging/fsm_example.svg @@ -1 +1 @@ -<svg id="mermaid-1650984468240" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="796" style="max-width: 1009.15px; background-color: white;" viewBox="0 0 1009.15234375 796"><style>#mermaid-1650984468240 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-1650984468240 .error-icon{fill:#552222;}#mermaid-1650984468240 .error-text{fill:#552222;stroke:#552222;}#mermaid-1650984468240 .edge-thickness-normal{stroke-width:2px;}#mermaid-1650984468240 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-1650984468240 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-1650984468240 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-1650984468240 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-1650984468240 .marker{fill:#333333;stroke:#333333;}#mermaid-1650984468240 .marker.cross{stroke:#333333;}#mermaid-1650984468240 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-1650984468240 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-1650984468240 .cluster-label text{fill:#333;}#mermaid-1650984468240 .cluster-label span{color:#333;}#mermaid-1650984468240 .label text,#mermaid-1650984468240 span{fill:#333;color:#333;}#mermaid-1650984468240 .node rect,#mermaid-1650984468240 .node circle,#mermaid-1650984468240 .node ellipse,#mermaid-1650984468240 .node polygon,#mermaid-1650984468240 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-1650984468240 .node .label{text-align:center;}#mermaid-1650984468240 .node.clickable{cursor:pointer;}#mermaid-1650984468240 .arrowheadPath{fill:#333333;}#mermaid-1650984468240 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-1650984468240 .flowchart-link{stroke:#333333;fill:none;}#mermaid-1650984468240 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-1650984468240 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-1650984468240 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-1650984468240 .cluster text{fill:#333;}#mermaid-1650984468240 .cluster span{color:#333;}#mermaid-1650984468240 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-1650984468240 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-Deregistered LE-No_Server" id="L-Deregistered-No_Server" style="opacity: 1;"><path class="path" d="M377.39453125,46L377.39453125,51.666666666666664C377.39453125,57.333333333333336,377.39453125,68.66666666666667,377.39453125,80C377.39453125,91.33333333333333,377.39453125,102.66666666666667,377.39453125,108.33333333333333L377.39453125,114" marker-end="url(#arrowhead71)" style="fill:none"></path><defs><marker id="arrowhead71" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-No_Server LE-Chosen_Server" id="L-No_Server-Chosen_Server" style="opacity: 1;"><path class="path" d="M377.39453125,152L377.39453125,157.66666666666666C377.39453125,163.33333333333334,377.39453125,174.66666666666666,377.39453125,186C377.39453125,197.33333333333334,377.39453125,208.66666666666666,377.39453125,214.33333333333334L377.39453125,220" marker-end="url(#arrowhead72)" style="fill:none"></path><defs><marker id="arrowhead72" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Chosen_Server LE-Authorized" id="L-Chosen_Server-Authorized" style="opacity: 1;"><path class="path" d="M377.39453125,258L377.39453125,263.6666666666667C377.39453125,269.3333333333333,377.39453125,280.6666666666667,377.39453125,292C377.39453125,303.3333333333333,377.39453125,314.6666666666667,377.39453125,320.3333333333333L377.39453125,326" marker-end="url(#arrowhead73)" style="fill:none"></path><defs><marker id="arrowhead73" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Chosen_Server LE-OAuth_Started" id="L-Chosen_Server-OAuth_Started" style="opacity: 1;"><path class="path" d="M311.80859375,251.45392075910038L276.220703125,258.21160063258367C240.6328125,264.9692805060669,169.45703125,278.48464025303343,133.869140625,294.07565345985006C98.28125,309.6666666666667,98.28125,327.3333333333333,98.28125,345C98.28125,362.6666666666667,98.28125,380.3333333333333,128.71028645833334,395.7359725470853C159.13932291666666,411.1386117608372,219.99739583333334,424.2772235216744,250.42643229166666,430.846529402093L280.85546875,437.4158352825115" marker-end="url(#arrowhead74)" style="fill:none"></path><defs><marker id="arrowhead74" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-OAuth_Started LE-Authorized" id="L-OAuth_Started-Authorized" style="opacity: 1;"><path class="path" d="M369.5760613207547,432L377.27041568396226,426.3333333333333C384.9647700471698,420.6666666666667,400.35347877358487,409.3333333333333,403.9477692610063,398C407.5420597484277,386.6666666666667,399.3419319968554,375.3333333333333,395.2418681210692,369.6666666666667L391.141804245283,364" marker-end="url(#arrowhead75)" style="fill:none"></path><defs><marker id="arrowhead75" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-OAuth_Started LE-Chosen_Server" id="L-OAuth_Started-Chosen_Server" style="opacity: 1;"><path class="path" d="M406.69921875,434.7339430313423L430.3821614583333,428.6116191927852C454.0651041666667,422.4892953542282,501.4309895833333,410.2446476771141,525.1139322916666,395.2889905052237C548.796875,380.3333333333333,548.796875,362.6666666666667,548.796875,345C548.796875,327.3333333333333,548.796875,309.6666666666667,530.4708382468554,295.1666666666667C512.1448014937107,280.6666666666667,475.4927279874214,269.3333333333333,457.1666912342768,263.6666666666667L438.8406544811321,258" marker-end="url(#arrowhead76)" style="fill:none"></path><defs><marker id="arrowhead76" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Authorized LE-OAuth_Started" id="L-Authorized-OAuth_Started" style="opacity: 1;"><path class="path" d="M329.14453125,359.8714477181345L308.5266927083333,366.2262064317788C287.9088541666667,372.580965145423,246.67317708333334,385.2904825727115,240.8463910180818,397.31190795302246C235.01960495283018,409.3333333333333,264.60170990566036,420.6666666666667,279.3927623820755,426.3333333333333L294.18381485849056,432" marker-end="url(#arrowhead77)" style="fill:none"></path><defs><marker id="arrowhead77" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Authorized LE-Request_Config" id="L-Authorized-Request_Config" style="opacity: 1;"><path class="path" d="M425.64453125,354.29895882160764L463.4368489583333,361.582465684673C501.2291666666667,368.8659725477384,576.8138020833334,383.4329862738692,614.6061197916666,396.3831598036013C652.3984375,409.3333333333333,652.3984375,420.6666666666667,652.3984375,426.3333333333333L652.3984375,432" marker-end="url(#arrowhead78)" style="fill:none"></path><defs><marker id="arrowhead78" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Request_Config LE-Ask_Profile" id="L-Request_Config-Ask_Profile" style="opacity: 1;"><path class="path" d="M683.6585347877359,470L692.9817216981132,475.6666666666667C702.3049086084906,481.3333333333333,720.9512824292452,492.6666666666667,730.2744693396226,504C739.59765625,515.3333333333334,739.59765625,526.6666666666666,739.59765625,532.3333333333334L739.59765625,538" marker-end="url(#arrowhead79)" style="fill:none"></path><defs><marker id="arrowhead79" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Request_Config LE-Has_Config" id="L-Request_Config-Has_Config" style="opacity: 1;"><path class="path" d="M621.1383402122641,470L611.8151533018868,475.6666666666667C602.4919663915094,481.3333333333333,583.8455925707548,492.6666666666667,574.5224056603773,507.1666666666667C565.19921875,521.6666666666666,565.19921875,539.3333333333334,565.19921875,557C565.19921875,574.6666666666666,565.19921875,592.3333333333334,574.5224056603773,606.8333333333334C583.8455925707548,621.3333333333334,602.4919663915094,632.6666666666666,611.8151533018868,638.3333333333334L621.1383402122641,644" marker-end="url(#arrowhead80)" style="fill:none"></path><defs><marker id="arrowhead80" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Has_Config LE-Connected" id="L-Has_Config-Connected" style="opacity: 1;"><path class="path" d="M652.3984375,682L652.3984375,687.6666666666666C652.3984375,693.3333333333334,652.3984375,704.6666666666666,664.6927820361635,716C676.987126572327,727.3333333333334,701.5758156446541,738.6666666666666,713.8701601808176,744.3333333333334L726.1645047169811,750" marker-end="url(#arrowhead81)" style="fill:none"></path><defs><marker id="arrowhead81" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Ask_Profile LE-Has_Config" id="L-Ask_Profile-Has_Config" style="opacity: 1;"><path class="path" d="M739.59765625,576L739.59765625,581.6666666666666C739.59765625,587.3333333333334,739.59765625,598.6666666666666,730.2744693396227,610C720.9512824292452,621.3333333333334,702.3049086084906,632.6666666666666,692.9817216981132,638.3333333333334L683.6585347877359,644" marker-end="url(#arrowhead82)" style="fill:none"></path><defs><marker id="arrowhead82" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Connected LE-Authorized" id="L-Connected-Authorized" style="opacity: 1;"><path class="path" d="M816.08203125,751.2888162127385L832.2526041666666,745.4073468439487C848.4231770833334,739.5258774751591,880.7643229166666,727.7629387375795,896.9348958333334,713.0481360354564C913.10546875,698.3333333333334,913.10546875,680.6666666666666,913.10546875,663C913.10546875,645.3333333333334,913.10546875,627.6666666666666,913.10546875,610C913.10546875,592.3333333333334,913.10546875,574.6666666666666,913.10546875,557C913.10546875,539.3333333333334,913.10546875,521.6666666666666,913.10546875,504C913.10546875,486.3333333333333,913.10546875,468.6666666666667,913.10546875,451C913.10546875,433.3333333333333,913.10546875,415.6666666666667,831.8619791666666,398.7955938613505C750.6184895833334,381.92452105603434,588.1315104166666,365.84904211206873,506.8880208333333,357.8113026400859L425.64453125,349.7735631681031" marker-end="url(#arrowhead83)" style="fill:none"></path><defs><marker id="arrowhead83" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(377.39453125,80)" style="opacity: 1;"><g transform="translate(-53.3515625,-9)" class="label"><rect rx="0" ry="0" width="106.703125" height="18"></rect><foreignObject width="106.703125" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Deregistered-No_Server" class="edgeLabel L-LS-Deregistered' L-LE-No_Server">Client registers</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(377.39453125,186)" style="opacity: 1;"><g transform="translate(-80.03125,-9)" class="label"><rect rx="0" ry="0" width="160.0625" height="18"></rect><foreignObject width="160.0625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-No_Server-Chosen_Server" class="edgeLabel L-LS-No_Server' L-LE-Chosen_Server">User chooses a server</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(377.39453125,292)" style="opacity: 1;"><g transform="translate(-80.5,-9)" class="label"><rect rx="0" ry="0" width="161" height="18"></rect><foreignObject width="161" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Chosen_Server-Authorized" class="edgeLabel L-LS-Chosen_Server' L-LE-Authorized">Found tokens in config</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(98.28125,345)" style="opacity: 1;"><g transform="translate(-90.28125,-9)" class="label"><rect rx="0" ry="0" width="180.5625" height="18"></rect><foreignObject width="180.5625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Chosen_Server-OAuth_Started" class="edgeLabel L-LS-Chosen_Server' L-LE-OAuth_Started">No tokens found in config</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(415.7421875,398)" style="opacity: 1;"><g transform="translate(-103.1484375,-9)" class="label"><rect rx="0" ry="0" width="206.296875" height="18"></rect><foreignObject width="206.296875" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-OAuth_Started-Authorized" class="edgeLabel L-LS-OAuth_Started' L-LE-Authorized">User authorizes with browser</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(548.796875,345)" style="opacity: 1;"><g transform="translate(-49.8046875,-9)" class="label"><rect rx="0" ry="0" width="99.609375" height="18"></rect><foreignObject width="99.609375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-OAuth_Started-Chosen_Server" class="edgeLabel L-LS-OAuth_Started' L-LE-Chosen_Server">Cancel OAuth</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(205.4375,398)" style="opacity: 1;"><g transform="translate(-87.15625,-9)" class="label"><rect rx="0" ry="0" width="174.3125" height="18"></rect><foreignObject width="174.3125" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Authorized-OAuth_Started" class="edgeLabel L-LS-Authorized' L-LE-OAuth_Started">Re-authorize with OAuth</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(652.3984375,398)" style="opacity: 1;"><g transform="translate(-83.6015625,-9)" class="label"><rect rx="0" ry="0" width="167.203125" height="18"></rect><foreignObject width="167.203125" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Authorized-Request_Config" class="edgeLabel L-LS-Authorized' L-LE-Request_Config">Client requests a config</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(739.59765625,504)" style="opacity: 1;"><g transform="translate(-77.8203125,-9)" class="label"><rect rx="0" ry="0" width="155.640625" height="18"></rect><foreignObject width="155.640625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Request_Config-Ask_Profile" class="edgeLabel L-LS-Request_Config' L-LE-Ask_Profile">Multiple profiles found</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(565.19921875,557)" style="opacity: 1;"><g transform="translate(-88.9375,-9)" class="label"><rect rx="0" ry="0" width="177.875" height="18"></rect><foreignObject width="177.875" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Request_Config-Has_Config" class="edgeLabel L-LS-Request_Config' L-LE-Has_Config">Success, only one profile</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(652.3984375,716)" style="opacity: 1;"><g transform="translate(-77.8203125,-9)" class="label"><rect rx="0" ry="0" width="155.640625" height="18"></rect><foreignObject width="155.640625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Has_Config-Connected" class="edgeLabel L-LS-Has_Config' L-LE-Connected">OS reports connected</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(739.59765625,610)" style="opacity: 1;"><g transform="translate(-119.6171875,-9)" class="label"><rect rx="0" ry="0" width="239.234375" height="18"></rect><foreignObject width="239.234375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Ask_Profile-Has_Config" class="edgeLabel L-LS-Ask_Profile' L-LE-Has_Config">User chooses profile and success</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(913.10546875,557)" style="opacity: 1;"><g transform="translate(-88.046875,-9)" class="label"><rect rx="0" ry="0" width="176.09375" height="18"></rect><foreignObject width="176.09375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Connected-Authorized" class="edgeLabel L-LS-Connected' L-LE-Authorized">OS reports disconnected</span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="flowchart-Deregistered-39" transform="translate(377.39453125,27)" style="opacity: 1;"><rect rx="5" ry="5" x="-55.8046875" y="-19" width="111.609375" height="38" class="label-container" style="fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.8046875,-9)"><foreignObject width="91.609375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Deregistered</div></foreignObject></g></g></g><g class="node default" id="flowchart-No_Server-41" transform="translate(377.39453125,133)" style="opacity: 1;"><rect rx="5" ry="5" x="-48.2421875" y="-19" width="96.484375" height="38" class="label-container" style="fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.2421875,-9)"><foreignObject width="76.484375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">No_Server</div></foreignObject></g></g></g><g class="node default" id="flowchart-Chosen_Server-44" transform="translate(377.39453125,239)" style="opacity: 1;"><rect rx="5" ry="5" x="-65.5859375" y="-19" width="131.171875" height="38" class="label-container" style="fill:white;fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-55.5859375,-9)"><foreignObject width="111.171875" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Chosen_Server</div></foreignObject></g></g></g><g class="node default" id="flowchart-Authorized-47" transform="translate(377.39453125,345)" style="opacity: 1;"><rect rx="5" ry="5" x="-48.25" y="-19" width="96.5" height="38" class="label-container" style="fill:white;fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.25,-9)"><foreignObject width="76.5" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Authorized</div></foreignObject></g></g></g><g class="node default" id="flowchart-OAuth_Started-50" transform="translate(343.77734375,451)" style="opacity: 1;"><rect rx="5" ry="5" x="-62.921875" y="-19" width="125.84375" height="38" class="label-container" style="fill:white;fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-52.921875,-9)"><foreignObject width="105.84375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">OAuth_Started</div></foreignObject></g></g></g><g class="node default" id="flowchart-Request_Config-62" transform="translate(652.3984375,451)" style="opacity: 1;"><rect rx="5" ry="5" x="-67.375" y="-19" width="134.75" height="38" class="label-container" style="fill:white;fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-57.375,-9)"><foreignObject width="114.75" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Request_Config</div></foreignObject></g></g></g><g class="node default" id="flowchart-Ask_Profile-65" transform="translate(739.59765625,557)" style="opacity: 1;"><rect rx="5" ry="5" x="-50.4609375" y="-19" width="100.921875" height="38" class="label-container" style="fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.4609375,-9)"><foreignObject width="80.921875" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Ask_Profile</div></foreignObject></g></g></g><g class="node default" id="flowchart-Has_Config-68" transform="translate(652.3984375,663)" style="opacity: 1;"><rect rx="5" ry="5" x="-51.8046875" y="-19" width="103.609375" height="38" class="label-container" style="fill:cyan;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-41.8046875,-9)"><foreignObject width="83.609375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Has_Config</div></foreignObject></g></g></g><g class="node default" id="flowchart-Connected-71" transform="translate(767.38671875,769)" style="opacity: 1;"><rect rx="5" ry="5" x="-48.6953125" y="-19" width="97.390625" height="38" class="label-container" style="fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.6953125,-9)"><foreignObject width="77.390625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Connected</div></foreignObject></g></g></g></g></g></g></svg>
\ No newline at end of file +<svg id="mermaid-1652441732449" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="796" style="max-width: 1351.44px; background-color: white;" viewBox="0 0 1351.4375 796"><style>#mermaid-1652441732449 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-1652441732449 .error-icon{fill:#552222;}#mermaid-1652441732449 .error-text{fill:#552222;stroke:#552222;}#mermaid-1652441732449 .edge-thickness-normal{stroke-width:2px;}#mermaid-1652441732449 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-1652441732449 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-1652441732449 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-1652441732449 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-1652441732449 .marker{fill:#333333;stroke:#333333;}#mermaid-1652441732449 .marker.cross{stroke:#333333;}#mermaid-1652441732449 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-1652441732449 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-1652441732449 .cluster-label text{fill:#333;}#mermaid-1652441732449 .cluster-label span{color:#333;}#mermaid-1652441732449 .label text,#mermaid-1652441732449 span{fill:#333;color:#333;}#mermaid-1652441732449 .node rect,#mermaid-1652441732449 .node circle,#mermaid-1652441732449 .node ellipse,#mermaid-1652441732449 .node polygon,#mermaid-1652441732449 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-1652441732449 .node .label{text-align:center;}#mermaid-1652441732449 .node.clickable{cursor:pointer;}#mermaid-1652441732449 .arrowheadPath{fill:#333333;}#mermaid-1652441732449 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-1652441732449 .flowchart-link{stroke:#333333;fill:none;}#mermaid-1652441732449 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-1652441732449 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-1652441732449 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-1652441732449 .cluster text{fill:#333;}#mermaid-1652441732449 .cluster span{color:#333;}#mermaid-1652441732449 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-1652441732449 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-Deregistered LE-No_Server" id="L-Deregistered-No_Server" style="opacity: 1;"><path class="path" d="M855.8828125,46L855.8828125,51.666666666666664C855.8828125,57.333333333333336,855.8828125,68.66666666666667,855.8828125,80C855.8828125,91.33333333333333,855.8828125,102.66666666666667,855.8828125,108.33333333333333L855.8828125,114" marker-end="url(#arrowhead109)" style="fill:none"></path><defs><marker id="arrowhead109" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-No_Server LE-Chosen_Server" id="L-No_Server-Chosen_Server" style="opacity: 1;"><path class="path" d="M807.640625,136.6325343663113L698.369140625,144.86044530525942C589.09765625,153.08835624420752,370.5546875,169.54417812210377,261.283203125,183.43875572771856C152.01171875,197.33333333333334,152.01171875,208.66666666666666,152.01171875,214.33333333333334L152.01171875,220" marker-end="url(#arrowhead110)" style="fill:none"></path><defs><marker id="arrowhead110" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Chosen_Server LE-Authorized" id="L-Chosen_Server-Authorized" style="opacity: 1;"><path class="path" d="M209.98076356132077,258L227.26977692610066,263.6666666666667C244.55879029088052,269.3333333333333,279.1368170204403,280.6666666666667,296.42583038522014,292C313.71484375,303.3333333333333,313.71484375,314.6666666666667,313.71484375,320.3333333333333L313.71484375,326" marker-end="url(#arrowhead111)" style="fill:none"></path><defs><marker id="arrowhead111" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Chosen_Server LE-OAuth_Started" id="L-Chosen_Server-OAuth_Started" style="opacity: 1;"><path class="path" d="M132.74985259433961,258L127.00508549528301,263.6666666666667C121.26031839622641,269.3333333333333,109.77078419811319,280.6666666666667,104.0260170990566,295.1666666666667C98.28125,309.6666666666667,98.28125,327.3333333333333,98.28125,345C98.28125,362.6666666666667,98.28125,380.3333333333333,98.28125,398C98.28125,415.6666666666667,98.28125,433.3333333333333,98.28125,451C98.28125,468.6666666666667,98.28125,486.3333333333333,119.537109375,501.081704507387C140.79296875,515.8300756814406,183.3046875,527.6601513628813,204.560546875,533.5751892036016L225.81640625,539.4902270443218" marker-end="url(#arrowhead112)" style="fill:none"></path><defs><marker id="arrowhead112" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-OAuth_Started LE-Authorized" id="L-OAuth_Started-Authorized" style="opacity: 1;"><path class="path" d="M335.38826650943395,538L349.3014200078616,532.3333333333334C363.2145735062893,526.6666666666666,391.0408805031446,515.3333333333334,404.9540340015723,500.8333333333333C418.8671875,486.3333333333333,418.8671875,468.6666666666667,418.8671875,451C418.8671875,433.3333333333333,418.8671875,415.6666666666667,407.6244840801887,401.1666666666667C396.3817806603774,386.6666666666667,373.8963738207547,375.3333333333333,362.65367040094344,369.6666666666667L351.4109669811321,364" marker-end="url(#arrowhead113)" style="fill:none"></path><defs><marker id="arrowhead113" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-OAuth_Started LE-No_Server" id="L-OAuth_Started-No_Server" style="opacity: 1;"><path class="path" d="M351.66015625,544.1957405324334L384.5813802083333,537.4964504436945C417.5026041666667,530.7971603549556,483.3450520833333,517.3985801774778,516.2662760416666,501.86595675540553C549.1875,486.3333333333333,549.1875,468.6666666666667,549.1875,451C549.1875,433.3333333333333,549.1875,415.6666666666667,549.1875,398C549.1875,380.3333333333333,549.1875,362.6666666666667,549.1875,345C549.1875,327.3333333333333,549.1875,309.6666666666667,549.1875,292C549.1875,274.3333333333333,549.1875,256.6666666666667,549.1875,239C549.1875,221.33333333333334,549.1875,203.66666666666666,592.2630208333334,187.3894549592005C635.3385416666666,171.1122432517343,721.4895833333334,156.2244865034686,764.5651041666666,148.78060812933575L807.640625,141.3367297552029" marker-end="url(#arrowhead114)" style="fill:none"></path><defs><marker id="arrowhead114" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Authorized LE-OAuth_Started" id="L-Authorized-OAuth_Started" style="opacity: 1;"><path class="path" d="M276.0187205188679,364L264.77601709905656,369.6666666666667C253.53331367924525,375.3333333333333,231.04790683962264,386.6666666666667,219.8052034198113,401.1666666666667C208.5625,415.6666666666667,208.5625,433.3333333333333,208.5625,451C208.5625,468.6666666666667,208.5625,486.3333333333333,217.13475334119497,500.8333333333333C225.70700668238996,515.3333333333334,242.85151336477986,526.6666666666666,251.42376670597483,532.3333333333334L259.9960200471698,538" marker-end="url(#arrowhead115)" style="fill:none"></path><defs><marker id="arrowhead115" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Authorized LE-Request_Config" id="L-Authorized-Request_Config" style="opacity: 1;"><path class="path" d="M361.96484375,352.5418591523335L410.435546875,360.1182159602779C458.90625,367.6945727682223,555.84765625,382.84728638411116,610.6236119300314,396.09030985872226C665.399567610063,409.3333333333333,678.0100727201258,420.6666666666667,684.3153252751572,426.3333333333333L690.6205778301887,432" marker-end="url(#arrowhead116)" style="fill:none"></path><defs><marker id="arrowhead116" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Request_Config LE-Ask_Profile" id="L-Request_Config-Ask_Profile" style="opacity: 1;"><path class="path" d="M750.6537441037735,470L762.2531200864779,475.6666666666667C773.8524960691824,481.3333333333333,797.0512480345911,492.6666666666667,818.85546875,504C840.6596894654089,515.3333333333334,861.0693789308176,526.6666666666666,871.2742236635221,532.3333333333334L881.4790683962265,538" marker-end="url(#arrowhead117)" style="fill:none"></path><defs><marker id="arrowhead117" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Request_Config LE-Has_Config" id="L-Request_Config-Has_Config" style="opacity: 1;"><path class="path" d="M677.5454746462265,470L667.3406299135221,475.6666666666667C657.1357851808176,481.3333333333333,636.7260957154089,492.6666666666667,626.5212509827044,507.1666666666667C616.31640625,521.6666666666666,616.31640625,539.3333333333334,616.31640625,557C616.31640625,574.6666666666666,616.31640625,592.3333333333334,671.5462239583334,608.8057733149838C726.7760416666666,625.2782132966341,837.2356770833334,640.5564265932684,892.4654947916666,648.1955332415855L947.6953125,655.8346398899026" marker-end="url(#arrowhead118)" style="fill:none"></path><defs><marker id="arrowhead118" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Request_Config LE-No_Server" id="L-Request_Config-No_Server" style="opacity: 1;"><path class="path" d="M760.8567216981132,432L775.4990909984277,426.3333333333333C790.1414602987421,420.6666666666667,819.4261988993711,409.3333333333333,834.0685681996856,394.8333333333333C848.7109375,380.3333333333333,848.7109375,362.6666666666667,848.7109375,345C848.7109375,327.3333333333333,848.7109375,309.6666666666667,848.7109375,292C848.7109375,274.3333333333333,848.7109375,256.6666666666667,848.7109375,239C848.7109375,221.33333333333334,848.7109375,203.66666666666666,849.477741745283,189.16666666666666C850.244545990566,174.66666666666666,851.778154481132,163.33333333333334,852.5449587264151,157.66666666666666L853.3117629716982,152" marker-end="url(#arrowhead119)" style="fill:none"></path><defs><marker id="arrowhead119" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Request_Config LE-OAuth_Started" id="L-Request_Config-OAuth_Started" style="opacity: 1;"><path class="path" d="M644.38671875,459.9709911678116L589.2747395833334,467.3091593065096C534.1627604166666,474.64732744520774,423.9388020833333,489.32366372260384,366.15637283805034,502.3284985279686C308.3739435927673,515.3333333333334,303.0330434355346,526.6666666666666,300.36259335691824,532.3333333333334L297.6921432783019,538" marker-end="url(#arrowhead120)" style="fill:none"></path><defs><marker id="arrowhead120" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Has_Config LE-Connected" id="L-Has_Config-Connected" style="opacity: 1;"><path class="path" d="M966.1841833726415,682L956.2478871855346,687.6666666666666C946.3115909984277,693.3333333333334,926.4389986242139,704.6666666666666,926.4389986242137,716C926.4389986242139,727.3333333333334,946.3115909984277,738.6666666666666,956.2478871855346,744.3333333333334L966.1841833726415,750" marker-end="url(#arrowhead121)" style="fill:none"></path><defs><marker id="arrowhead121" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Has_Config LE-Request_Config" id="L-Has_Config-Request_Config" style="opacity: 1;"><path class="path" d="M1036.245283018868,644L1047.2044025157231,638.3333333333334C1058.1635220125786,632.6666666666666,1080.0817610062893,621.3333333333334,1091.0408805031445,606.8333333333334C1102,592.3333333333334,1102,574.6666666666666,1102,557C1102,539.3333333333334,1102,521.6666666666666,1048.189453125,505.52508316566735C994.37890625,489.383499664668,886.7578125,474.766999329336,832.947265625,467.45874916167L779.13671875,460.15049899400407" marker-end="url(#arrowhead122)" style="fill:none"></path><defs><marker id="arrowhead122" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Has_Config LE-No_Server" id="L-Has_Config-No_Server" style="opacity: 1;"><path class="path" d="M1051.3046875,650.4623452606044L1079.1692708333333,643.7186210505037C1107.0338541666667,636.974896840403,1162.7630208333333,623.4874484202014,1190.6276041666667,607.9103908767673C1218.4921875,592.3333333333334,1218.4921875,574.6666666666666,1218.4921875,557C1218.4921875,539.3333333333334,1218.4921875,521.6666666666666,1218.4921875,504C1218.4921875,486.3333333333333,1218.4921875,468.6666666666667,1218.4921875,451C1218.4921875,433.3333333333333,1218.4921875,415.6666666666667,1218.4921875,398C1218.4921875,380.3333333333333,1218.4921875,362.6666666666667,1218.4921875,345C1218.4921875,327.3333333333333,1218.4921875,309.6666666666667,1218.4921875,292C1218.4921875,274.3333333333333,1218.4921875,256.6666666666667,1218.4921875,239C1218.4921875,221.33333333333334,1218.4921875,203.66666666666666,1166.09765625,187.17520216601312C1113.703125,170.6837376653596,1008.9140625,155.36747533071917,956.51953125,147.70934416339898L904.125,140.05121299607876" marker-end="url(#arrowhead123)" style="fill:none"></path><defs><marker id="arrowhead123" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Ask_Profile LE-Has_Config" id="L-Ask_Profile-Has_Config" style="opacity: 1;"><path class="path" d="M915.6953125,576L915.6953125,581.6666666666666C915.6953125,587.3333333333334,915.6953125,598.6666666666666,924.6555621069183,610C933.6158117138365,621.3333333333334,951.5363109276728,632.6666666666666,960.4965605345911,638.3333333333334L969.4568101415094,644" marker-end="url(#arrowhead124)" style="fill:none"></path><defs><marker id="arrowhead124" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Ask_Profile LE-No_Server" id="L-Ask_Profile-No_Server" style="opacity: 1;"><path class="path" d="M944.7694575471698,538L953.4406937893082,532.3333333333334C962.1119300314466,526.6666666666666,979.4544025157232,515.3333333333334,988.1256387578616,500.8333333333333C996.796875,486.3333333333333,996.796875,468.6666666666667,996.796875,451C996.796875,433.3333333333333,996.796875,415.6666666666667,996.796875,398C996.796875,380.3333333333333,996.796875,362.6666666666667,996.796875,345C996.796875,327.3333333333333,996.796875,309.6666666666667,996.796875,292C996.796875,274.3333333333333,996.796875,256.6666666666667,996.796875,239C996.796875,221.33333333333334,996.796875,203.66666666666666,981.3515625,189.0241078523775C965.90625,174.38154903808837,935.015625,162.76309807617676,919.5703125,156.95387259522093L904.125,151.14464711426513" marker-end="url(#arrowhead125)" style="fill:none"></path><defs><marker id="arrowhead125" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-Connected LE-Has_Config" id="L-Connected-Has_Config" style="opacity: 1;"><path class="path" d="M1032.8158166273586,750L1042.7521128144656,744.3333333333334C1052.6884090015724,738.6666666666666,1072.5610013757862,727.3333333333334,1072.5610013757862,716C1072.5610013757862,704.6666666666666,1052.6884090015724,693.3333333333334,1042.7521128144656,687.6666666666666L1032.8158166273586,682" marker-end="url(#arrowhead126)" style="fill:none"></path><defs><marker id="arrowhead126" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(855.8828125,80)" style="opacity: 1;"><g transform="translate(-53.3515625,-9)" class="label"><rect rx="0" ry="0" width="106.703125" height="18"></rect><foreignObject width="106.703125" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Deregistered-No_Server" class="edgeLabel L-LS-Deregistered' L-LE-No_Server">Client registers</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(152.01171875,186)" style="opacity: 1;"><g transform="translate(-80.03125,-9)" class="label"><rect rx="0" ry="0" width="160.0625" height="18"></rect><foreignObject width="160.0625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-No_Server-Chosen_Server" class="edgeLabel L-LS-No_Server' L-LE-Chosen_Server">User chooses a server</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(313.71484375,292)" style="opacity: 1;"><g transform="translate(-80.5,-9)" class="label"><rect rx="0" ry="0" width="161" height="18"></rect><foreignObject width="161" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Chosen_Server-Authorized" class="edgeLabel L-LS-Chosen_Server' L-LE-Authorized">Found tokens in config</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(98.28125,398)" style="opacity: 1;"><g transform="translate(-90.28125,-9)" class="label"><rect rx="0" ry="0" width="180.5625" height="18"></rect><foreignObject width="180.5625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Chosen_Server-OAuth_Started" class="edgeLabel L-LS-Chosen_Server' L-LE-OAuth_Started">No tokens found in config</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(418.8671875,451)" style="opacity: 1;"><g transform="translate(-103.1484375,-9)" class="label"><rect rx="0" ry="0" width="206.296875" height="18"></rect><foreignObject width="206.296875" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-OAuth_Started-Authorized" class="edgeLabel L-LS-OAuth_Started' L-LE-Authorized">User authorizes with browser</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(549.1875,345)" style="opacity: 1;"><g transform="translate(-54.2421875,-9)" class="label"><rect rx="0" ry="0" width="108.484375" height="18"></rect><foreignObject width="108.484375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-OAuth_Started-No_Server" class="edgeLabel L-LS-OAuth_Started' L-LE-No_Server">Cancel or Error</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(208.5625,451)" style="opacity: 1;"><g transform="translate(-87.15625,-9)" class="label"><rect rx="0" ry="0" width="174.3125" height="18"></rect><foreignObject width="174.3125" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Authorized-OAuth_Started" class="edgeLabel L-LS-Authorized' L-LE-OAuth_Started">Re-authorize with OAuth</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(652.7890625,398)" style="opacity: 1;"><g transform="translate(-83.6015625,-9)" class="label"><rect rx="0" ry="0" width="167.203125" height="18"></rect><foreignObject width="167.203125" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Authorized-Request_Config" class="edgeLabel L-LS-Authorized' L-LE-Request_Config">Client requests a config</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(820.25,504)" style="opacity: 1;"><g transform="translate(-156.546875,-9)" class="label"><rect rx="0" ry="0" width="313.09375" height="18"></rect><foreignObject width="313.09375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Request_Config-Ask_Profile" class="edgeLabel L-LS-Request_Config' L-LE-Ask_Profile">Multiple profiles found and no profile chosen</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(616.31640625,557)" style="opacity: 1;"><g transform="translate(-145.859375,-9)" class="label"><rect rx="0" ry="0" width="291.71875" height="18"></rect><foreignObject width="291.71875" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Request_Config-Has_Config" class="edgeLabel L-LS-Request_Config' L-LE-Has_Config">Only one profile or profile already chosen</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(848.7109375,292)" style="opacity: 1;"><g transform="translate(-54.2421875,-9)" class="label"><rect rx="0" ry="0" width="108.484375" height="18"></rect><foreignObject width="108.484375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Request_Config-No_Server" class="edgeLabel L-LS-Request_Config' L-LE-No_Server">Cancel or Error</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(313.71484375,504)" style="opacity: 1;"><g transform="translate(-45.8046875,-9)" class="label"><rect rx="0" ry="0" width="91.609375" height="18"></rect><foreignObject width="91.609375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Request_Config-OAuth_Started" class="edgeLabel L-LS-Request_Config' L-LE-OAuth_Started">Re-authorize</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(906.56640625,716)" style="opacity: 1;"><g transform="translate(-77.8203125,-9)" class="label"><rect rx="0" ry="0" width="155.640625" height="18"></rect><foreignObject width="155.640625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Has_Config-Connected" class="edgeLabel L-LS-Has_Config' L-LE-Connected">OS reports connected</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1102,557)" style="opacity: 1;"><g transform="translate(-96.4921875,-9)" class="label"><rect rx="0" ry="0" width="192.984375" height="18"></rect><foreignObject width="192.984375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Has_Config-Request_Config" class="edgeLabel L-LS-Has_Config' L-LE-Request_Config">User chooses a new profile</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1218.4921875,398)" style="opacity: 1;"><g transform="translate(-124.9453125,-9)" class="label"><rect rx="0" ry="0" width="249.890625" height="18"></rect><foreignObject width="249.890625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Has_Config-No_Server" class="edgeLabel L-LS-Has_Config' L-LE-No_Server">User wants to choose a new server</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(915.6953125,610)" style="opacity: 1;"><g transform="translate(-72.921875,-9)" class="label"><rect rx="0" ry="0" width="145.84375" height="18"></rect><foreignObject width="145.84375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Ask_Profile-Has_Config" class="edgeLabel L-LS-Ask_Profile' L-LE-Has_Config">User chooses profile</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(996.796875,345)" style="opacity: 1;"><g transform="translate(-99.625,-9)" class="label"><rect rx="0" ry="0" width="199.25" height="18"></rect><foreignObject width="199.25" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Ask_Profile-No_Server" class="edgeLabel L-LS-Ask_Profile' L-LE-No_Server">Done but no profile selected</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1092.43359375,716)" style="opacity: 1;"><g transform="translate(-88.046875,-9)" class="label"><rect rx="0" ry="0" width="176.09375" height="18"></rect><foreignObject width="176.09375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span id="L-L-Connected-Has_Config" class="edgeLabel L-LS-Connected' L-LE-Has_Config">OS reports disconnected</span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="flowchart-Deregistered-54" transform="translate(855.8828125,27)" style="opacity: 1;"><rect rx="5" ry="5" x="-55.8046875" y="-19" width="111.609375" height="38" class="label-container" style="fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.8046875,-9)"><foreignObject width="91.609375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Deregistered</div></foreignObject></g></g></g><g class="node default" id="flowchart-No_Server-56" transform="translate(855.8828125,133)" style="opacity: 1;"><rect rx="5" ry="5" x="-48.2421875" y="-19" width="96.484375" height="38" class="label-container" style="fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.2421875,-9)"><foreignObject width="76.484375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">No_Server</div></foreignObject></g></g></g><g class="node default" id="flowchart-Chosen_Server-59" transform="translate(152.01171875,239)" style="opacity: 1;"><rect rx="5" ry="5" x="-65.5859375" y="-19" width="131.171875" height="38" class="label-container" style="fill:white;fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-55.5859375,-9)"><foreignObject width="111.171875" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Chosen_Server</div></foreignObject></g></g></g><g class="node default" id="flowchart-Authorized-62" transform="translate(313.71484375,345)" style="opacity: 1;"><rect rx="5" ry="5" x="-48.25" y="-19" width="96.5" height="38" class="label-container" style="fill:white;fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.25,-9)"><foreignObject width="76.5" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Authorized</div></foreignObject></g></g></g><g class="node default" id="flowchart-OAuth_Started-65" transform="translate(288.73828125,557)" style="opacity: 1;"><rect rx="5" ry="5" x="-62.921875" y="-19" width="125.84375" height="38" class="label-container" style="fill:white;fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-52.921875,-9)"><foreignObject width="105.84375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">OAuth_Started</div></foreignObject></g></g></g><g class="node default" id="flowchart-Request_Config-77" transform="translate(711.76171875,451)" style="opacity: 1;"><rect rx="5" ry="5" x="-67.375" y="-19" width="134.75" height="38" class="label-container" style="fill:white;fill:white;fill:white;fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-57.375,-9)"><foreignObject width="114.75" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Request_Config</div></foreignObject></g></g></g><g class="node default" id="flowchart-Ask_Profile-80" transform="translate(915.6953125,557)" style="opacity: 1;"><rect rx="5" ry="5" x="-50.4609375" y="-19" width="100.921875" height="38" class="label-container" style="fill:white;fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.4609375,-9)"><foreignObject width="80.921875" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Ask_Profile</div></foreignObject></g></g></g><g class="node default" id="flowchart-Has_Config-83" transform="translate(999.5,663)" style="opacity: 1;"><rect rx="5" ry="5" x="-51.8046875" y="-19" width="103.609375" height="38" class="label-container" style="fill:cyan;fill:cyan;fill:cyan;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-41.8046875,-9)"><foreignObject width="83.609375" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Has_Config</div></foreignObject></g></g></g><g class="node default" id="flowchart-Connected-92" transform="translate(999.5,769)" style="opacity: 1;"><rect rx="5" ry="5" x="-48.6953125" y="-19" width="97.390625" height="38" class="label-container" style="fill:white;"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.6953125,-9)"><foreignObject width="77.390625" height="18"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Connected</div></foreignObject></g></g></g></g></g></g></svg>
\ No newline at end of file diff --git a/docs/src/godifferences.png b/docs/src/godifferences.png Binary files differnew file mode 100644 index 0000000..916afd2 --- /dev/null +++ b/docs/src/godifferences.png diff --git a/exports/exports.go b/exports/exports.go index 919f5d7..287a4b6 100644 --- a/exports/exports.go +++ b/exports/exports.go @@ -5,7 +5,6 @@ package main typedef void (*PythonCB)(const char* oldstate, const char* newstate, const char* data); -// FIXME: Remove this, see: https://stackoverflow.com/questions/58606884/multiple-definition-when-using-cgo __attribute__((weak)) void call_callback(PythonCB callback, const char* oldstate, const char* newstate, const char* data) { @@ -49,9 +48,11 @@ func GetVPNState(name string) (*eduvpn.VPNState, error) { //export Register func Register(name *C.char, config_directory *C.char, stateCallback C.PythonCB, debug C.int) *C.char { - state := &eduvpn.VPNState{} nameStr := C.GoString(name) - + state, stateErr := GetVPNState(nameStr) + if stateErr != nil { + state = &eduvpn.VPNState{} + } if VPNStates == nil { VPNStates = make(map[string]*eduvpn.VPNState) } @@ -97,21 +98,22 @@ func CancelOAuth(name *C.char) *C.char { } //export GetConnectConfig -func GetConnectConfig(name *C.char, url *C.char, isSecureInternet C.int, forceTCP C.int) (*C.char, *C.char) { +func GetConnectConfig(name *C.char, url *C.char, isSecureInternet C.int, forceTCP C.int) (*C.char, *C.char, *C.char) { nameStr := C.GoString(name) state, stateErr := GetVPNState(nameStr) if stateErr != nil { - return nil, C.CString(ErrorToString(stateErr)) + return nil, nil, C.CString(ErrorToString(stateErr)) } var config string + var configType string var configErr error forceTCPBool := forceTCP == 1 if isSecureInternet == 1 { - config, configErr = state.GetConfigSecureInternet(C.GoString(url), forceTCPBool) + config, configType, configErr = state.GetConfigSecureInternet(C.GoString(url), forceTCPBool) } else { - config, configErr = state.GetConfigInstituteAccess(C.GoString(url), forceTCPBool) + config, configType, configErr = state.GetConfigInstituteAccess(C.GoString(url), forceTCPBool) } - return C.CString(config), C.CString(ErrorToString(configErr)) + return C.CString(config), C.CString(configType), C.CString(ErrorToString(configErr)) } //export GetOrganizationsList diff --git a/internal/openvpn.go b/internal/openvpn.go index 45beb51..7c4df18 100644 --- a/internal/openvpn.go +++ b/internal/openvpn.go @@ -2,20 +2,20 @@ package internal import "fmt" -func OpenVPNGetConfig(server Server) (string, error) { +func OpenVPNGetConfig(server Server) (string, string, error) { base, baseErr := server.GetBase() if baseErr != nil { - return "", &OpenVPNGetConfigError{Err: baseErr} + return "", "", &OpenVPNGetConfigError{Err: baseErr} } profile_id := base.Profiles.Current configOpenVPN, _, configErr := APIConnectOpenVPN(server, profile_id) if configErr != nil { - return "", &OpenVPNGetConfigError{Err: configErr} + return "", "", &OpenVPNGetConfigError{Err: configErr} } - return configOpenVPN, nil + return configOpenVPN, "openvpn", nil } type OpenVPNGetConfigError struct { diff --git a/internal/server.go b/internal/server.go index c9e31af..95140af 100644 --- a/internal/server.go +++ b/internal/server.go @@ -305,19 +305,19 @@ func getCurrentProfile(server Server) (*ServerProfile, error) { return nil, &ServerGetCurrentProfileNotFoundError{ProfileID: profileID} } -func getConfigWithProfile(server Server, forceTCP bool) (string, error) { +func getConfigWithProfile(server Server, forceTCP bool) (string, string, error) { base, baseErr := server.GetBase() if baseErr != nil { - return "", &ServerGetConfigWithProfileError{Err: baseErr} + return "", "", &ServerGetConfigWithProfileError{Err: baseErr} } if !base.FSM.HasTransition(HAS_CONFIG) { - return "", &FSMWrongStateTransitionError{Got: base.FSM.Current, Want: HAS_CONFIG} + return "", "", &FSMWrongStateTransitionError{Got: base.FSM.Current, Want: HAS_CONFIG} } profile, profileErr := getCurrentProfile(server) if profileErr != nil { - return "", &ServerGetConfigWithProfileError{Err: profileErr} + return "", "", &ServerGetConfigWithProfileError{Err: profileErr} } supportsOpenVPN := profile.supportsOpenVPN() @@ -325,15 +325,26 @@ func getConfigWithProfile(server Server, forceTCP bool) (string, error) { // If forceTCP we must be able to get a config with OpenVPN if forceTCP && supportsOpenVPN { - return "", &ServerGetConfigForceTCPError{} + return "", "", &ServerGetConfigForceTCPError{} } + var config string + var configType string + var configErr error + if supportsWireguard { // A wireguard connect call needs to generate a wireguard key and add it to the config // Also the server could send back an OpenVPN config if it supports OpenVPN - return WireguardGetConfig(server, supportsOpenVPN) + config, configType, configErr = WireguardGetConfig(server, supportsOpenVPN) + } else { + config, configType, configErr = OpenVPNGetConfig(server) + } + + if configErr != nil { + return "", "", &ServerGetConfigWithProfileError{Err: configErr} } - return OpenVPNGetConfig(server) + + return config, configType, nil } func askForProfileID(server Server) error { @@ -349,21 +360,21 @@ func askForProfileID(server Server) error { return nil } -func GetConfig(server Server, forceTCP bool) (string, error) { +func GetConfig(server Server, forceTCP bool) (string, string, error) { base, baseErr := server.GetBase() if baseErr != nil { - return "", &ServerGetConfigError{Err: baseErr} + return "", "", &ServerGetConfigError{Err: baseErr} } if !base.FSM.InState(REQUEST_CONFIG) { - return "", &FSMWrongStateError{Got: base.FSM.Current, Want: REQUEST_CONFIG} + return "", "", &FSMWrongStateError{Got: base.FSM.Current, Want: REQUEST_CONFIG} } // Get new profiles using the info call // This does not override the current profile infoErr := APIInfo(server) if infoErr != nil { - return "", &ServerGetConfigError{Err: infoErr} + return "", "", &ServerGetConfigError{Err: infoErr} } // If there was a profile chosen and it doesn't exist anymore, reset it @@ -387,7 +398,7 @@ func GetConfig(server Server, forceTCP bool) (string, error) { profileErr := askForProfileID(server) if profileErr != nil { - return "", &ServerGetConfigError{Err: profileErr} + return "", "", &ServerGetConfigError{Err: profileErr} } return getConfigWithProfile(server, forceTCP) diff --git a/internal/wireguard.go b/internal/wireguard.go index 7f8da38..6edad08 100644 --- a/internal/wireguard.go +++ b/internal/wireguard.go @@ -30,29 +30,29 @@ func wireguardConfigAddKey(config string, key wgtypes.Key) string { return interface_re.ReplaceAllString(config, to_replace) } -func WireguardGetConfig(server Server, supportsOpenVPN bool) (string, error) { +func WireguardGetConfig(server Server, supportsOpenVPN bool) (string, string, error) { base, baseErr := server.GetBase() if baseErr != nil { - return "", &WireguardGetConfigError{Err: baseErr} + return "", "", &WireguardGetConfigError{Err: baseErr} } profile_id := base.Profiles.Current wireguardKey, wireguardErr := wireguardGenerateKey() if wireguardErr != nil { - return "", &WireguardGetConfigError{Err: wireguardErr} + return "", "", &WireguardGetConfigError{Err: wireguardErr} } wireguardPublicKey := wireguardKey.PublicKey().String() config, content, _, configErr := APIConnectWireguard(server, profile_id, wireguardPublicKey, supportsOpenVPN) if configErr != nil { - return "", &WireguardGetConfigError{Err: wireguardErr} + return "", "", &WireguardGetConfigError{Err: wireguardErr} } + // FIXME: Store expiry if content == "wireguard" { - // FIXME: Store expiry // This needs the go code a way to identify a connection // Use the uuid of the connection e.g. on Linux // This needs the client code to call the go code @@ -60,7 +60,7 @@ func WireguardGetConfig(server Server, supportsOpenVPN bool) (string, error) { config = wireguardConfigAddKey(config, wireguardKey) } - return config, nil + return config, content, nil } type WireguardGenerateKeyError struct { @@ -100,22 +100,21 @@ func (state *VPNState) chooseServer(url string, isSecureInternet bool) (internal return server, nil } -func (state *VPNState) getConfigWithOptions(url string, isSecureInternet bool, forceTCP bool) (string, error) { - // FIXME: Do something with force tcp +func (state *VPNState) getConfigWithOptions(url string, isSecureInternet bool, forceTCP bool) (string, string, error) { if state.FSM.InState(internal.DEREGISTERED) { - return "", &StateFSMNotRegisteredError{} + return "", "", &StateFSMNotRegisteredError{} } // Go to no server if possible, else return an error if !state.FSM.InState(internal.NO_SERVER) && !state.FSM.GoTransition(internal.NO_SERVER) { - return "", &internal.FSMWrongStateTransitionError{Got: state.FSM.Current, Want: internal.NO_SERVER} + return "", "", &internal.FSMWrongStateTransitionError{Got: state.FSM.Current, Want: internal.NO_SERVER} } // Make sure the server is chosen server, serverErr := state.chooseServer(url, isSecureInternet) if serverErr != nil { - return "", &StateConnectError{URL: url, IsSecureInternet: isSecureInternet, Err: serverErr} + return "", "", &StateConnectError{URL: url, IsSecureInternet: isSecureInternet, Err: serverErr} } // Relogin with oauth // This moves the state to authorized @@ -126,7 +125,7 @@ func (state *VPNState) getConfigWithOptions(url string, isSecureInternet bool, f // We are possibly in oauth started // So go to no server state.FSM.GoTransition(internal.NO_SERVER) - return "", &StateConnectError{URL: url, IsSecureInternet: isSecureInternet, Err: loginErr} + return "", "", &StateConnectError{URL: url, IsSecureInternet: isSecureInternet, Err: loginErr} } } else { // OAuth was valid, ensure we are in the authorized state state.FSM.GoTransition(internal.AUTHORIZED) @@ -134,24 +133,24 @@ func (state *VPNState) getConfigWithOptions(url string, isSecureInternet bool, f state.FSM.GoTransition(internal.REQUEST_CONFIG) - config, configErr := internal.GetConfig(server, forceTCP) + config, configType, configErr := internal.GetConfig(server, forceTCP) if configErr != nil { // Go back to no server if possible state.FSM.GoTransition(internal.NO_SERVER) - return "", &StateConnectError{URL: url, IsSecureInternet: isSecureInternet, Err: configErr} + return "", "", &StateConnectError{URL: url, IsSecureInternet: isSecureInternet, Err: configErr} } else { state.FSM.GoTransition(internal.HAS_CONFIG) } - return config, nil + return config, configType, nil } -func (state *VPNState) GetConfigInstituteAccess(url string, forceTCP bool) (string, error) { +func (state *VPNState) GetConfigInstituteAccess(url string, forceTCP bool) (string, string, error) { return state.getConfigWithOptions(url, false, forceTCP) } -func (state *VPNState) GetConfigSecureInternet(url string, forceTCP bool) (string, error) { +func (state *VPNState) GetConfigSecureInternet(url string, forceTCP bool) (string, string, error) { return state.getConfigWithOptions(url, true, forceTCP) } diff --git a/state_test.go b/state_test.go index 21b3abd..b515ffb 100644 --- a/state_test.go +++ b/state_test.go @@ -68,7 +68,7 @@ func Test_server(t *testing.T) { stateCallback(t, old, new, data, state) }, false) - _, configErr := state.GetConfigInstituteAccess(serverURI, false) + _, _, configErr := state.GetConfigInstituteAccess(serverURI, false) if configErr != nil { t.Fatalf("Connect error: %v", configErr) @@ -91,7 +91,7 @@ func test_connect_oauth_parameter(t *testing.T, parameters internal.URLParameter } }, false) - _, configErr := state.GetConfigInstituteAccess(serverURI, false) + _, _, configErr := state.GetConfigInstituteAccess(serverURI, false) var stateErr *StateConnectError var loginErr *internal.OAuthLoginError @@ -171,7 +171,7 @@ func Test_token_expired(t *testing.T) { stateCallback(t, old, new, data, state) }, false) - _, configErr := state.GetConfigInstituteAccess(serverURI, false) + _, _, configErr := state.GetConfigInstituteAccess(serverURI, false) if configErr != nil { t.Fatalf("Connect error before expired: %v", configErr) @@ -219,7 +219,7 @@ func Test_token_invalid(t *testing.T) { stateCallback(t, old, new, data, state) }, false) - _, configErr := state.GetConfigInstituteAccess(serverURI, false) + _, _, configErr := state.GetConfigInstituteAccess(serverURI, false) if configErr != nil { t.Fatalf("Connect error before invalid: %v", configErr) @@ -243,7 +243,7 @@ func Test_token_invalid(t *testing.T) { oauth.Token.Access = dummy_value oauth.Token.Refresh = dummy_value - _, configErr = state.GetConfigInstituteAccess(serverURI, false) + _, _, configErr = state.GetConfigInstituteAccess(serverURI, false) if configErr != nil { t.Fatalf("Connect error after invalid: %v", configErr) @@ -269,7 +269,7 @@ func Test_invalid_profile_corrected(t *testing.T) { stateCallback(t, old, new, data, state) }, false) - _, configErr := state.GetConfigInstituteAccess(serverURI, false) + _, _, configErr := state.GetConfigInstituteAccess(serverURI, false) if configErr != nil { t.Fatalf("First connect error: %v", configErr) @@ -288,7 +288,7 @@ func Test_invalid_profile_corrected(t *testing.T) { previousProfile := base.Profiles.Current base.Profiles.Current = "IDONOTEXIST" - _, configErr = state.GetConfigInstituteAccess(serverURI, false) + _, _, configErr = state.GetConfigInstituteAccess(serverURI, false) if configErr != nil { t.Fatalf("Second connect error: %v", configErr) diff --git a/wrappers/python/main.py b/wrappers/python/main.py index 1c1afd7..c887fed 100644 --- a/wrappers/python/main.py +++ b/wrappers/python/main.py @@ -1,32 +1,90 @@ import eduvpncommon.main as eduvpn import webbrowser +import json +# Asks the user for a profile index +# It loops up until a valid input is given +def ask_profile_input(total: int) -> int: + profile_index = None -_eduvpn = eduvpn.EduVPN("org.eduvpn.app.linux", "configs") + while profile_index is None: + try: + profile_index = int( + input("Please select a profile by inputting a number (e.g. 1): ") + ) + if (profile_index > total) or (profile_index < 1): + print("Invalid profile range") + profile_index = None + except ValueError: + print("Please enter a valid input") + # The profile is one based, move to zero based input + return profile_index - 1 -@_eduvpn.event.on("OAuth_Started", eduvpn.StateType.Enter) -def oauth_initialized(url): - print(f"Got OAUTH url {url}") - webbrowser.open(url) +# Sets up the callbacks using the provided class +def setup_callbacks(_eduvpn: eduvpn.EduVPN) -> None: + # The callback that starst OAuth + # It needs to open the URL in the web browser + @_eduvpn.event.on("OAuth_Started", eduvpn.StateType.Enter) + def oauth_initialized(old_state: str, url: str) -> None: + print(f"Got OAuth URL {url}, old state: {old_state}") + webbrowser.open(url) + # The callback which asks the user for a profile + @_eduvpn.event.on("Ask_Profile", eduvpn.StateType.Enter) + def ask_profile(old_state: str, profiles: str): + print("Multiple profiles found, you need to select a profile, old state: {old_state}") -@_eduvpn.event.on("Ask_Profile", eduvpn.StateType.Enter) -def ask_profile(profiles): - print("ASK PROFILE CB", profiles) - _eduvpn.set_profile("prefer-openvpn") + # Parse the profiles as JSON + data = json.loads(profiles) + # Get a lits of profiles + profile_strings = [x["profile_id"] for x in data["info"]["profile_list"]] + total_profiles = len(profile_strings) -success = _eduvpn.register(debug=True) + # Create a list of the strings to standard output + for idx, profile in enumerate(profile_strings): + print(f"{idx+1}. {profile}") -if not success: - print("failed to register") + # Get the profile index from the user + profile_index = ask_profile_input(total_profiles) -print(_eduvpn.get_disco()) + # Set the profile with the index + _eduvpn.set_profile(profile_strings[profile_index]) -config, error = _eduvpn.get_config_institute_access("https://eduvpn.jwijenbergh.com") -if error: - print("Got connect error", error) +# The main entry point +if __name__ == "__main__": + _eduvpn = eduvpn.EduVPN("org.eduvpn.app.linux", "configs") + setup_callbacks(_eduvpn) -print(config) + # Register with the eduVPN-common library + try: + _eduvpn.register(debug=True) + except Exception as e: + print("Failed registering:", e) + + server = input( + "Which Institute Access server do you want to connect to? (e.g. https://eduvpn.example.com): " + ) + + # Ensure we have a valid http prefix + if not server.startswith("http"): + # https by default + server = "https://" + server + + # Get a Wireguard/OpenVPN config + try: + config, config_type = _eduvpn.get_config_institute_access(server) + except Exception as e: + print("Failed to connect:", e) + print(f"Got a config with type: {config_type} and contents:\n{config}") + + # Set the internal FSM state to connected + try: + _eduvpn.set_connected() + except Exception as e: + print("Failed to set connected:", e) + + # Save and exit + _eduvpn.deregister() diff --git a/wrappers/python/src/__init__.py b/wrappers/python/src/__init__.py index c028f09..c96a1b2 100644 --- a/wrappers/python/src/__init__.py +++ b/wrappers/python/src/__init__.py @@ -2,6 +2,7 @@ from ctypes import * from collections import defaultdict import pathlib import platform +from typing import Tuple _lib_prefixes = defaultdict( lambda: "lib", @@ -30,15 +31,31 @@ class DataError(Structure): _fields_ = [("data", c_void_p), ("error", c_void_p)] +class MultipleDataError(Structure): + _fields_ = [("data", c_void_p), ("other_data", c_void_p), ("error", c_void_p)] + + VPNStateChange = CFUNCTYPE(None, c_char_p, c_char_p, c_char_p) # Exposed functions # We have to use c_void_p instead of c_char_p to free it properly # See https://stackoverflow.com/questions/13445568/python-ctypes-how-to-free-memory-getting-invalid-pointer-error -lib.GetConnectConfig.argtypes, lib.GetConnectConfig.restype = [c_char_p, c_char_p, c_int, c_int], DataError +lib.GetConnectConfig.argtypes, lib.GetConnectConfig.restype = [ + c_char_p, + c_char_p, + c_int, + c_int, +], MultipleDataError lib.Deregister.argtypes, lib.Deregister.restype = [c_char_p], c_void_p -lib.Register.argtypes, lib.Register.restype = [c_char_p, c_char_p, VPNStateChange, c_int], c_void_p -lib.GetOrganizationsList.argtypes, lib.GetOrganizationsList.restype = [c_char_p], DataError +lib.Register.argtypes, lib.Register.restype = [ + c_char_p, + c_char_p, + VPNStateChange, + c_int, +], c_void_p +lib.GetOrganizationsList.argtypes, lib.GetOrganizationsList.restype = [ + c_char_p +], DataError lib.GetServersList.argtypes, lib.GetServersList.restype = [c_char_p], DataError lib.CancelOAuth.argtypes, lib.CancelOAuth.restype = [c_char_p], c_void_p lib.SetProfileID.argtypes, lib.SetProfileID.restype = [c_char_p, c_char_p], c_void_p @@ -47,7 +64,7 @@ lib.SetDisconnected.argtypes, lib.SetDisconnected.restype = [c_char_p], c_void_p lib.FreeString.argtypes, lib.FreeString.restype = [c_void_p], None -def GetPtrString(ptr): +def GetPtrString(ptr: c_void_p) -> str: if ptr: string = cast(ptr, c_char_p).value lib.FreeString(ptr) @@ -56,7 +73,16 @@ def GetPtrString(ptr): return "" -def GetDataError(data_error): +def GetDataError(data_error: DataError) -> Tuple[str, str]: data = GetPtrString(data_error.data) error = GetPtrString(data_error.error) return data, error + + +def GetMultipleDataError( + multiple_data_error: MultipleDataError, +) -> Tuple[str, str, str]: + data = GetPtrString(multiple_data_error.data) + other_data = GetPtrString(multiple_data_error.other_data) + error = GetPtrString(multiple_data_error.error) + return data, other_data, error diff --git a/wrappers/python/src/main.py b/wrappers/python/src/main.py index 2b346e3..dd8f36a 100644 --- a/wrappers/python/src/main.py +++ b/wrappers/python/src/main.py @@ -1,6 +1,7 @@ -from . import lib, VPNStateChange, GetDataError, GetPtrString +from . import lib, VPNStateChange, GetDataError, GetMultipleDataError, GetPtrString from ctypes import * from enum import Enum +from typing import Callable, Optional, Tuple class StateType(Enum): @@ -8,47 +9,94 @@ class StateType(Enum): Leave = 2 +class EventHandler(object): + def __init__(self): + self.handlers = {} + + def on(self, state: str, state_type: StateType) -> Callable: + def wrapped_f(func): + if (state, state_type) not in self.handlers: + self.handlers[(state, state_type)] = [] + self.handlers[(state, state_type)].append(func) + return func + + return wrapped_f + + def run_state(self, state: str, other_state: str, state_type: StateType, data: str) -> None: + if (state, state_type) not in self.handlers: + return + for func in self.handlers[(state, state_type)]: + func(other_state, data) + + def run(self, old_state: str, new_state: str, data: str) -> None: + if old_state == new_state: + return + self.run_state(old_state, new_state, StateType.Leave, data) + self.run_state(new_state, old_state, StateType.Enter, data) + + # Registers the python app with the Go code # name: The name of the app to be registered # state_callback: The callback to trigger whenever a state is changed -def Register(name, config_directory, state_callback, debug): +def Register( + name: str, config_directory: str, state_callback: Optional[Callable], debug: bool +) -> str: + if not state_callback: + return "No callback provided" name_bytes = name.encode("utf-8") dir_bytes = config_directory.encode("utf-8") ptr_err = lib.Register(name_bytes, dir_bytes, state_callback, debug) err_string = GetPtrString(ptr_err) return err_string -def CancelOAuth(name): + +def CancelOAuth(name: str) -> str: name_bytes = name.encode("utf-8") ptr_err = lib.CancelOAuth(name_bytes) err_string = GetPtrString(ptr_err) return err_string -def Deregister(name): + +def Deregister(name: str) -> str: name_bytes = name.encode("utf-8") ptr_err = lib.Deregister(name_bytes) err_string = GetPtrString(ptr_err) return err_string -def GetDiscoServers(name): + +def GetDiscoServers(name: str) -> Tuple[str, str]: + name_bytes = name.encode("utf-8") + servers, servers_err = GetDataError(lib.GetServersList(name_bytes)) + return servers, servers_err + + +def GetDiscoOrganizations(name: str) -> Tuple[str, str]: name_bytes = name.encode("utf-8") - servers, serversErr = GetDataError(lib.GetServersList(name_bytes)) - organizations, organizationsErr = GetDataError(lib.GetOrganizationsList(name_bytes)) - return servers, serversErr, organizations, organizationsErr + organizations, organizations_err = GetDataError( + lib.GetOrganizationsList(name_bytes) + ) + return organizations, organizations_err + -def GetConnectConfig(name, url, is_secure_internet, force_tcp): +def GetConnectConfig( + name: str, url: str, is_secure_internet: bool, force_tcp: bool +) -> Tuple[str, str, str]: name_bytes = name.encode("utf-8") url_bytes = url.encode("utf-8") - data_error = lib.GetConnectConfig(name_bytes, url_bytes, is_secure_internet, force_tcp) - return GetDataError(data_error) + multiple_data_error = lib.GetConnectConfig( + name_bytes, url_bytes, is_secure_internet, force_tcp + ) + return GetMultipleDataError(multiple_data_error) + -def SetConnected(name): +def SetConnected(name: str) -> str: name_bytes = name.encode("utf-8") ptr_err = lib.SetConnected(name_bytes) err_string = GetPtrString(ptr_err) return err_string -def SetDisconnected(name): + +def SetDisconnected(name: str) -> str: name_bytes = name.encode("utf-8") ptr_err = lib.SetDisconnected(name_bytes) err_string = GetPtrString(ptr_err) @@ -68,7 +116,7 @@ def register_callback(eduvpn): ) -def SetProfileID(name, profile_id) -> str: +def SetProfileID(name: str, profile_id: str) -> str: name_bytes = name.encode("utf-8") profile_bytes = profile_id.encode("utf-8") error_string = lib.SetProfileID(name_bytes, profile_bytes) @@ -76,68 +124,93 @@ def SetProfileID(name, profile_id) -> str: class EduVPN(object): - def __init__(self, name, config_directory): + def __init__(self, name: str, config_directory: str): self.event_handler = EventHandler() self.name = name self.config_directory = config_directory register_callback(self) - def cancel_oauth(self) -> str: - return CancelOAuth(self.name) + def cancel_oauth(self) -> None: + cancel_oauth_err = CancelOAuth(self.name) - def deregister(self) -> str: - return Deregister(self.name) + if cancel_oauth_err: + raise Exception(cancel_oauth_err) - def register(self, debug=False) -> bool: - return Register(self.name, self.config_directory, callback_function, debug) == "" + def deregister(self) -> None: + deregister_err = Deregister(self.name) - def get_disco(self): - return GetDiscoServers(self.name) + if deregister_err: + raise Exception(deregister_err) - def get_config_institute_access(self, url, force_tcp=False): - return GetConnectConfig(self.name, url, False, force_tcp) + def register(self, debug: bool = False) -> None: + register_err = Register( + self.name, self.config_directory, callback_function, debug + ) - def get_config_secure_internet(self, url, force_tcp=False): - return GetConnectConfig(self.name, url, True, force_tcp) + if register_err: + raise Exception(register_err) - def set_disconnected(self): - return SetDisconnected(self.name) + def get_disco_servers(self) -> str: + servers, servers_err = GetDiscoServers(self.name) - def set_connected(self): - return SetConnected(self.name) + if servers_err: + raise Exception(servers_err) - @property - def event(self): - return self.event_handler + return servers - def callback(self, old_state, new_state, data): - self.event.run(old_state, new_state, data) + def get_disco_organizations(self) -> str: + organizations, organizations_err = GetDiscoOrganizations(self.name) - def set_profile(self, profile_id) -> str: - return SetProfileID(self.name, profile_id) + if organizations_err: + raise Exception(organizations_err) + return organizations -class EventHandler(object): - def __init__(self): - self.handlers = {} + def get_config_institute_access( + self, url: str, force_tcp: bool = False + ) -> Tuple[str, str]: + config, config_type, config_err = GetConnectConfig( + self.name, url, False, force_tcp + ) - def on(self, state, state_type): - def wrapped_f(func): - if (state, state_type) not in self.handlers: - self.handlers[(state, state_type)] = [] - self.handlers[(state, state_type)].append(func) - return func + if config_err: + raise Exception(config_err) - return wrapped_f + return config, config_type - def run_state(self, state, state_type, data): - if (state, state_type) not in self.handlers: - return - for func in self.handlers[(state, state_type)]: - func(data) + def get_config_secure_internet( + self, url: str, force_tcp: bool = False + ) -> Tuple[str, str]: + config, config_type, config_err = GetConnectConfig( + self.name, url, True, force_tcp + ) - def run(self, old_state, new_state, data): - if old_state == new_state: - return - self.run_state(old_state, StateType.Leave, data) - self.run_state(new_state, StateType.Enter, data) + if config_err: + raise Exception(config_err) + + return config, config_type + + def set_disconnected(self) -> None: + disconnect_err = SetDisconnected(self.name) + + if disconnect_err: + raise Exception(disconnect_err) + + def set_connected(self) -> None: + connect_err = SetConnected(self.name) + + if connect_err: + raise Exception(connect_err) + + @property + def event(self) -> EventHandler: + return self.event_handler + + def callback(self, old_state: str, new_state: str, data: str) -> None: + self.event.run(old_state, new_state, data) + + def set_profile(self, profile_id: str) -> None: + profile_err = SetProfileID(self.name, profile_id) + + if profile_err: + raise Exception(profile_err) diff --git a/wrappers/python/tests.py b/wrappers/python/tests.py index f006646..60ed79e 100644 --- a/wrappers/python/tests.py +++ b/wrappers/python/tests.py @@ -13,20 +13,33 @@ from selenium_eduvpn import login_eduvpn class ConfigTests(unittest.TestCase): def testConfig(self): - self._eduvpn = eduvpn.EduVPN("org.eduvpn.app.linux", "testconfigs") - assert self._eduvpn.register() - @self._eduvpn.event.on("OAuth_Started", eduvpn.StateType.Enter) - def oauth_initialized(url): + _eduvpn = eduvpn.EduVPN("org.eduvpn.app.linux", "testconfigs") + # This can throw an exception + _eduvpn.register() + @_eduvpn.event.on("OAuth_Started", eduvpn.StateType.Enter) + def oauth_initialized(old_state, url): login_eduvpn(url) server_uri = os.getenv("SERVER_URI") if not server_uri: self.fail("No SERVER_URI environment variable given") - config, error = self._eduvpn.get_config_institute_access(server_uri) - - if error != "": - self.fail(f"Got error: {error} when connecting to {server_uri}") + # This can throw an exception + _eduvpn.get_config_institute_access(server_uri) + + # Deregister + _eduvpn.deregister() + + def testDoubleRegister(self): + _eduvpn = eduvpn.EduVPN("org.eduvpn.app.linux", "testconfigs") + # This can throw an exception + _eduvpn.register() + # This should throw + try: + _eduvpn.register() + except Exception as e: + return + self.fail("No exception thrown on second register") if __name__ == "__main__": unittest.main() |
