diff options
| -rw-r--r-- | wrappers/python/eduvpn_common/discovery.py | 71 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/error.py | 7 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/event.py | 76 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/loader.py | 14 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/main.py | 261 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/server.py | 123 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/state.py | 6 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/types.py | 133 |
8 files changed, 670 insertions, 21 deletions
diff --git a/wrappers/python/eduvpn_common/discovery.py b/wrappers/python/eduvpn_common/discovery.py index be9d079..63cc7a0 100644 --- a/wrappers/python/eduvpn_common/discovery.py +++ b/wrappers/python/eduvpn_common/discovery.py @@ -11,6 +11,13 @@ from eduvpn_common.types import ( class DiscoOrganization: + """The class that represents an organization from discovery + + :param: display_name: str: The display name of the organizations + :param: org_id: str: The organization ID + :param: secure_internet_home: str: Indicating which server is the secure internet home server + :param: keyword_list: The list of strings that the users gets to search on to find the server + """ def __init__(self, display_name: str, org_id: str, secure_internet_home: str, keyword_list: List[str]): self.display_name = display_name self.org_id = org_id @@ -22,12 +29,29 @@ class DiscoOrganization: class DiscoOrganizations: + """The class that represents the list of disco organizations from discovery. + Additionally it has provided a version which indicates which exact 'version' was used from discovery + + :param: version: int: The version of the list as returned by Discovery + :param: organizations: List[DiscoOrganizations]: The actual list of discovery organizations + """ def __init__(self, version: int, organizations: List[DiscoOrganization]): self.version = version self.organizations = organizations class DiscoServer: + """The class that represents a discovery server, this can be an institute access or secure internet server + + :param: authentication_url_template: str: The OAuth template to use to skip WAYF + :param: base_url: str: The base URL of the server + :param: country_code: str: The country code of the server + :param: display_name: str: The display name of the server + :param: keyword_list: List[str]: The list of keywords that the user can use to find the server + :param: public_keys: List[str]: The list of public keys + :param: server_type: str: The server type as a string + :param: support_contacts: List[str]: The list of support contacts + """ def __init__( self, authentication_url_template: str, @@ -53,12 +77,27 @@ class DiscoServer: class DiscoServers: - def __init__(self, version, servers): + """This class represents the list of discovery servers. + The version indicates which exact 'version' from Discovery was used. + + :param: version: int: The version of the list as returned by Discovery + :param: servers: List[DiscoServers]: The list of discovery servers + """ + def __init__(self, version: int, servers: List[DiscoServer]): self.version = version self.servers = servers def get_disco_organization(ptr) -> Optional[DiscoOrganization]: + """Gets a discovery organization from the Go library in a C structure and returns a Python usable structure + + :param ptr: The pointer returned by the go library that contains a discovery organization + + :meta: private: + + :return: The Discovery Organization if there is one + :rtype: Optional[DiscoOrganization] + """ if not ptr: return None @@ -71,6 +110,16 @@ def get_disco_organization(ptr) -> Optional[DiscoOrganization]: def get_disco_server(lib: CDLL, ptr) -> Optional[DiscoServer]: + """Gets a discovery server from the Go library in a C structure and returns a Python usable structure + + :param lib: CDLL: The Go shared library + :param ptr: The pointer to a discovery server returned by the Go library + + :meta: private: + + :return: The Discovery Server if there is one + :rtype: Optional[DiscoServer] + """ if not ptr: return None @@ -102,6 +151,16 @@ def get_disco_server(lib: CDLL, ptr) -> Optional[DiscoServer]: def get_disco_servers(lib: CDLL, ptr: c_void_p) -> Optional[DiscoServers]: + """Gets servers from the Go library in a C structure and returns a Python usable structure + + :param lib: CDLL: The Go shared library + :param ptr: c_void_p: The pointer returned by the Go library for the discovery servers + + :meta: private: + + :return: The Discovery Servers if there are any + :rtype: Optional[DiscoServers] + """ if ptr: svrs = cast(ptr, POINTER(cDiscoveryServers)).contents @@ -120,6 +179,16 @@ def get_disco_servers(lib: CDLL, ptr: c_void_p) -> Optional[DiscoServers]: def get_disco_organizations(lib: CDLL, ptr: c_void_p) -> Optional[DiscoOrganizations]: + """Gets organizations from the Go library in a C structure and returns a Python usable structure + + :param lib: CDLL: The Go shared library + :param ptr: c_void_p: The pointer returned by the Go library for the discovery organizations + + :meta: private: + + :return: The Discovery Organizations if there are any + :rtype: Optional[DiscoOrganizations] + """ if ptr: orgs = cast(ptr, POINTER(cDiscoveryOrganizations)).contents organizations = [] diff --git a/wrappers/python/eduvpn_common/error.py b/wrappers/python/eduvpn_common/error.py index a5b59b4..626d9a2 100644 --- a/wrappers/python/eduvpn_common/error.py +++ b/wrappers/python/eduvpn_common/error.py @@ -2,6 +2,7 @@ from enum import Enum class ErrorLevel(Enum): + """The error level enum""" ERR_OTHER = 0 ERR_INFO = 1 ERR_WARNING = 2 @@ -9,6 +10,12 @@ class ErrorLevel(Enum): class WrappedError(Exception): + """An exception returned by the Go library + + :param: traceback: str: The traceback of the error including newlines + :param: cause: str: The cause of the error as a message + :param: level: ErrorLevel: The level of the error + """ def __init__(self, traceback: str, cause: str, level: ErrorLevel): super(WrappedError, self).__init__(cause) self.traceback = traceback diff --git a/wrappers/python/eduvpn_common/event.py b/wrappers/python/eduvpn_common/event.py index e2bfabb..193e659 100644 --- a/wrappers/python/eduvpn_common/event.py +++ b/wrappers/python/eduvpn_common/event.py @@ -10,12 +10,25 @@ from eduvpn_common.server import ( from eduvpn_common.state import State, StateType from eduvpn_common.types import get_ptr_string +# The attribute that callback functions get EDUVPN_CALLBACK_PROPERTY = "_eduvpn_property_callback" # A state transition decorator for classes # To use this, make sure to register the class with `register_class_callbacks` def class_state_transition(state: int, state_type: StateType) -> Callable: + """A decorator to be internally by classes to register the event + + :param state: int: The state of the transition + :param state_type: StateType: The type of transition + + :meta: private: + """ def wrapper(func): + """ + + :param func: The function to set the internal attribute for + + """ setattr(func, EDUVPN_CALLBACK_PROPERTY, (state, state_type)) return func @@ -23,6 +36,14 @@ def class_state_transition(state: int, state_type: StateType) -> Callable: def convert_data(lib: CDLL, state: int, data: Any) -> None: + """The function that converts the C structure from the Go library to a Python structure + + :param lib: CDLL: The Go shared library + :param state: int: The state to convert the data for + :param data: Any: The data itself that has to be converted + + :meta: private: + """ if not data: return None if state is State.NO_SERVER: @@ -43,11 +64,19 @@ def convert_data(lib: CDLL, state: int, data: Any) -> None: class EventHandler(object): + """The class that neatly handles event callbacks from the internal Go FSM""" def __init__(self, lib: CDLL): self.handlers: Dict[Tuple[int, StateType], List[Callable]] = {} self.lib = lib def change_class_callbacks(self, cls: Any, add: bool = True) -> None: + """The function that is used to change class callbacks + + :param cls: Any: The class to change the callbacks for + :param add: bool: (Default value = True): Whether or not to add or remove the event. If true the event gets added + + :meta: private: + """ # Loop over method names for method_name in dir(cls): try: @@ -68,6 +97,14 @@ class EventHandler(object): self.remove_event(state, state_type, method) def remove_event(self, state: int, state_type: StateType, func: Callable) -> None: + """Removes an event + + :param state: int: The state to remove the event for + :param state_type: StateType: The state type to remove the event for + :param func: Callable: The function that needs to be removed from the event + + :meta: private: + """ for key, values in self.handlers.copy().items(): if key == (state, state_type): values.remove(func) @@ -77,13 +114,32 @@ class EventHandler(object): self.handlers[key] = values def add_event(self, state: int, state_type: StateType, func: Callable) -> None: + """Adds an event + + :param state: int: The state to add the event for + :param state_type: StateType: The state type to add the event for + :param func: Callable: The function that needs to be added to the event + + :meta: private: + """ if (state, state_type) not in self.handlers: self.handlers[(state, state_type)] = [] self.handlers[(state, state_type)].append(func) - # A decorator for standalone functions def on(self, state: int, state_type: StateType) -> Callable: + """The decorator for standalone functions + + :param state: int: The state of the event + :param state_type: StateType: The state type of the event + + :meta: private: + """ def wrapped_f(func): + """ + + :param func: The function to add the event for + + """ self.add_event(state, state_type, func) return func @@ -92,6 +148,15 @@ class EventHandler(object): def run_state( self, state: int, other_state: int, state_type: StateType, data: str ) -> None: + """The function that runs the callback for a specific event + + :param state: int: The state of the event + :param other_state: int: The other state of the event + :param state_type: StateType: The state type of the event + :param data: str: The data that gets passed to the function callback when the event is ran + + :meta: private: + """ if (state, state_type) not in self.handlers: return for func in self.handlers[(state, state_type)]: @@ -100,6 +165,15 @@ class EventHandler(object): def run( self, old_state: int, new_state: int, data: Any, convert: bool = True ) -> None: + """Run a specific event. + It converts the data and then runs the event for all state types + + :param old_state: int: The previous state for running the event + :param new_state: int: The new state for running the event + :param data: Any: The data that gets passed to the event + :param convert: bool: (Default value = True): Whether or not to convert the data further + + """ # First run leave transitions, then enter # The state is done when the wait event finishes converted = data diff --git a/wrappers/python/eduvpn_common/loader.py b/wrappers/python/eduvpn_common/loader.py index 170437a..f299f94 100644 --- a/wrappers/python/eduvpn_common/loader.py +++ b/wrappers/python/eduvpn_common/loader.py @@ -18,7 +18,12 @@ from eduvpn_common.types import ( ) -def load_lib(): +def load_lib() -> CDLL: + """The function that loads the Go shared library + + :return: The Go shared library loaded with cdll.LoadLibrary from ctypes + :rtype: CDLL + """ lib_prefixes = defaultdict( lambda: "lib", { @@ -51,7 +56,12 @@ def load_lib(): return lib -def initialize_functions(lib: CDLL): +def initialize_functions(lib: CDLL) -> None: + """Initializes the Go shared library functions + + :param lib: CDLL: The Go shared library + + """ # 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 diff --git a/wrappers/python/eduvpn_common/main.py b/wrappers/python/eduvpn_common/main.py index 49618d8..03e5b4c 100644 --- a/wrappers/python/eduvpn_common/main.py +++ b/wrappers/python/eduvpn_common/main.py @@ -1,16 +1,24 @@ import threading from ctypes import c_char_p, c_int, c_void_p -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple from eduvpn_common.discovery import DiscoOrganizations, DiscoServers, get_disco_organizations, get_disco_servers from eduvpn_common.event import EventHandler from eduvpn_common.loader import initialize_functions, load_lib -from eduvpn_common.server import Server, get_servers +from eduvpn_common.server import Profiles, Server, get_servers from eduvpn_common.state import State, StateType from eduvpn_common.types import VPNStateChange, decode_res, encode_args, get_data_error class EduVPN(object): + """The main class used to communicate with the Go library. + It registers the client with the library and then calls the needed appropriate functions + + :param name: str: The name of the client. For commonly used names, see https://git.sr.ht/~fkooman/vpn-user-portal/tree/v3/item/src/OAuth/ClientDb.php. E.g. org.eduvpn.app.linux, if this name has "letsconnect" in it, then it is a Let's Connect! variant + :param config_directory: str: The directory (absolute/relative) where to store the files + :param language: str: The language of the client, e.g. en + + """ def __init__(self, name: str, config_directory: str, language: str): self.name = name self.config_directory = config_directory @@ -30,16 +38,37 @@ class EduVPN(object): self.location_event: Optional[threading.Event] = None @self.event.on(State.ASK_PROFILE, StateType.WAIT) - def wait_profile_event(old_state: int, profiles: str): + def wait_profile_event(old_state: int, profiles: Profiles): + """This functions waits until the ask location thread event is finished + + :param old_state: int: The old state of the profiles event + :param profiles: Profiles: The profiles + + """ if self.profile_event: self.profile_event.wait() @self.event.on(State.ASK_LOCATION, StateType.WAIT) - def wait_location_event(old_state: int, locations: str): + def wait_location_event(old_state: int, locations: List[str]): + """This functions waits until the location thread event is finished + + :param old_state: int: The old state of the location event + :param locations: List[str]: The locations + + """ if self.location_event: self.location_event.wait() - def go_function(self, func: Any, *args, decode_func: Optional[Callable] = None) -> Any: + def go_function(self, func: Any, *args: Iterator, decode_func: Optional[Callable] = None) -> Any: + """Call an internal go function and properly forward the arguments. + Also handles decoding the result + + :param func: Any: The Go function to call from the shared library + :param \*args: Iterator: The arguments to call the function with + :param decode_func: Optional[Callable]: (Default value = None): The function to decode the result into a Python type + + :meta private: + """ # The functions all have at least one arg type which is the name of the client args_gen = encode_args(list(args), func.argtypes[1:]) res = func(self.name.encode("utf-8"), *(args_gen)) @@ -49,16 +78,26 @@ class EduVPN(object): return decode_func(self.lib, res) def cancel_oauth(self) -> None: + """Cancel the OAuth process""" cancel_oauth_err = self.go_function(self.lib.CancelOAuth) if cancel_oauth_err: raise cancel_oauth_err def deregister(self) -> None: + """Deregister the Go shared library. + This removes the object from internal bookkeeping and saves the configuration + """ self.go_function(self.lib.Deregister) remove_as_global_object(self) def register(self, debug: bool = False) -> None: + """Register the Go shared library. + This makes sure the FSM is initialized and that we can call Go functions + + :param debug: bool: (Default value = False): Whether or not we want to enable debug logging + + """ if not add_as_global_object(self): raise Exception("Already registered") @@ -74,6 +113,13 @@ class EduVPN(object): raise register_err def get_disco_servers(self) -> Optional[DiscoServers]: + """Get the discovery servers + + :raises WrappedError: An error by the Go library + + :return: The disco Servers if any + :rtype: Optional[DiscoServers] + """ servers, servers_err = self.go_function( self.lib.GetDiscoServers, decode_func=lambda lib, x: get_data_error(lib, x, get_disco_servers), @@ -85,6 +131,13 @@ class EduVPN(object): return servers def get_disco_organizations(self) -> Optional[DiscoOrganizations]: + """Get the discovery organizations + + :raises WrappedError: An error by the Go library + + :return: The discovery Organizations if any + :rtype: Optional[DiscoOrganizations] + """ organizations, organizations_err = self.go_function( self.lib.GetDiscoOrganizations, decode_func=lambda lib, x: get_data_error(lib, x, get_disco_organizations), @@ -95,19 +148,25 @@ class EduVPN(object): return organizations - def remove_secure_internet(self) -> None: - remove_err = self.go_function(self.lib.RemoveSecureInternet) + def add_institute_access(self, url: str) -> None: + """Add an institute access server - if remove_err: - raise remove_err + :param url: str: The URL for the institute access server. Use the exact base_url as returned by Discovery - def add_institute_access(self, url: str) -> None: + :raises WrappedError: An error by the Go library + """ add_err = self.go_function(self.lib.AddInstituteAccess, url) if add_err: raise add_err def add_secure_internet_home(self, org_id: str) -> None: + """Add a secure internet server + + :param org_id: str: The organization ID of the secure internet server. Use the exact organization as returned by Discovery + + :raises WrappedError: An error by the Go library + """ self.location_event = threading.Event() add_err = self.go_function(self.lib.AddSecureInternetHomeServer, org_id) @@ -115,30 +174,71 @@ class EduVPN(object): raise add_err def add_custom_server(self, url: str) -> None: + """Add a custom server + + :param url: str: The base URL of the server + + :raises WrappedError: An error by the Go library + """ add_err = self.go_function(self.lib.AddCustomServer, url) if add_err: raise add_err + def remove_secure_internet(self) -> None: + """Remove the secure internet server + + :raises WrappedError: An error by the Go library + """ + remove_err = self.go_function(self.lib.RemoveSecureInternet) + + if remove_err: + raise remove_err + def remove_institute_access(self, url: str) -> None: + """Remove an institute access server + + :param url: str: The URL for the institute access server. Use the exact base_url as returned by Discovery + + :raises WrappedError: An error by the Go library + """ remove_err = self.go_function(self.lib.RemoveInstituteAccess, url) if remove_err: raise remove_err def remove_custom_server(self, url: str) -> None: + """Remove a custom server + + :param url: str: The base URL of the server + + :raises WrappedError: An error by the Go library + """ remove_err = self.go_function(self.lib.RemoveCustomServer, url) if remove_err: raise remove_err - def get_config(self, url: str, func: Any, prefer_tcp: bool = False) -> Tuple[str, str]: + def get_config(self, identifier: str, func: Any, prefer_tcp: bool = False) -> Tuple[str, str]: + """Get an OpenVPN/WireGuard configuration from the server + + :param identifier: str: The identifier of the server, e.g. URL or ORG ID + :param func: Any: The Go function to call + :param prefer_tcp: bool: (Default value = False): Whether or not to prefer TCP + + :meta: private: + + :raises WrappedError: An error by the Go library + + :return: The configuration and configuration type ('openvpn' or 'wireguard') + :rtype: Tuple[str, str] + """ # Because it could be the case that a profile callback is started, store a threading event # In the constructor, we have defined a wait event for Ask_Profile, this waits for this event to be set # The event is set in self.set_profile self.profile_event = threading.Event() - config, config_type, config_err = self.go_function(func, url, prefer_tcp) + config, config_type, config_err = self.go_function(func, identifier, prefer_tcp) self.profile_event = None self.location_event = None @@ -151,66 +251,144 @@ class EduVPN(object): def get_config_custom_server( self, url: str, prefer_tcp: bool = False ) -> Tuple[str, str]: + """Get an OpenVPN/WireGuard configuration from a custom server + + :param url: str: The URL of the custom server + :param prefer_tcp: bool: (Default value = False): Whether or not to prefer TCP + + :raises WrappedError: An error by the Go library + + :return: The configuration and configuration type ('openvpn' or 'wireguard') + :rtype: Tuple[str, str] + """ return self.get_config(url, self.lib.GetConfigCustomServer, prefer_tcp) def get_config_institute_access( self, url: str, prefer_tcp: bool = False ) -> Tuple[str, str]: + """Get an OpenVPN/WireGuard configuration from an institute access server + + :param url: str: The URL of the institute access server. Use the one from Discovery + :param prefer_tcp: bool: (Default value = False): Whether or not to prefer TCP + + :raises WrappedError: An error by the Go library + + :return: The configuration and configuration type ('openvpn' or 'wireguard') + :rtype: Tuple[str, str] + """ return self.get_config(url, self.lib.GetConfigInstituteAccess, prefer_tcp) def get_config_secure_internet( - self, url: str, prefer_tcp: bool = False + self, org_id: str, prefer_tcp: bool = False ) -> Tuple[str, str]: - return self.get_config(url, self.lib.GetConfigSecureInternet, prefer_tcp) + """Get an OpenVPN/WireGuard configuration from a secure internet server + + :param org_id: str: The organization ID of the secure internet server. Use the one from Discovery + :param prefer_tcp: bool: (Default value = False): Whether or not to prefer TCP + + :raises WrappedError: An error by the Go library + """ + return self.get_config(org_id, self.lib.GetConfigSecureInternet, prefer_tcp) def go_back(self) -> None: + """Go back in the FSM""" # Ignore the error self.go_function(self.lib.GoBack) def set_connected(self) -> None: + """Set the FSM to connected + + :raises WrappedError: An error by the Go library + """ connect_err = self.go_function(self.lib.SetConnected) if connect_err: raise connect_err def set_disconnecting(self) -> None: + """Set the FSM to disconnecting + + :raises WrappedError: An error by the Go library + """ disconnecting_err = self.go_function(self.lib.SetDisconnecting) if disconnecting_err: raise disconnecting_err def set_connecting(self) -> None: + """Set the FSM to connecting + + :raises WrappedError: An error by the Go library + """ connecting_err = self.go_function(self.lib.SetConnecting) if connecting_err: raise connecting_err def set_disconnected(self, cleanup: bool = True) -> None: + """Set the FSM to disconnected + + :param cleanup: bool: (Default value = True): Whether or not to call /disconnect to the server. This invalidates the OpenVPN/WireGuard configuration + + :raises WrappedError: An error by the Go library + """ disconnect_err = self.go_function(self.lib.SetDisconnected, cleanup) if disconnect_err: raise disconnect_err def set_search_server(self) -> None: + """Set the FSM to search server + + :raises WrappedError: An error by the Go library + """ search_err = self.go_function(self.lib.SetSearchServer) if search_err: raise search_err def remove_class_callbacks(self, cls: Any) -> None: + """Remove class callbacks + + :param cls: Any: The class to remove callbacks for + + """ self.event_handler.change_class_callbacks(cls, add=False) def register_class_callbacks(self, cls: Any) -> None: + """Register class callbacks + + :param cls: Any: The class to register callbacks for + + """ self.event_handler.change_class_callbacks(cls) @property def event(self) -> EventHandler: + """The property that gets the event handler + + :return: The event handler + :rtype: EventHandler + """ return self.event_handler def callback(self, old_state: State, new_state: State, data: Any) -> None: + """Run an event callback + + :param old_state: State: The previous state + :param new_state: State: The new state + :param data: Any: The data to pass to the event + + """ self.event.run(old_state, new_state, data) def set_profile(self, profile_id: str) -> None: + """Set the profile of the current server + + :param profile_id: str: The profile id of the chosen profile for the server + + :raises WrappedError: An error by the Go library + """ # Set the profile id profile_err = self.go_function(self.lib.SetProfileID, profile_id) @@ -223,6 +401,10 @@ class EduVPN(object): raise profile_err def change_secure_location(self) -> None: + """Change the secure location. This calls the necessary events + + :raises WrappedError: An error by the Go library + """ # Set the location by country code self.location_event = threading.Event() location_err = self.go_function(self.lib.ChangeSecureLocation) @@ -231,6 +413,12 @@ class EduVPN(object): raise location_err def set_secure_location(self, country_code: str) -> None: + """Set the secure location + + :param country_code: str: The country code of the new location + + :raises WrappedError: An error by the Go library + """ # Set the location by country code location_err = self.go_function(self.lib.SetSecureLocation, country_code) @@ -243,18 +431,39 @@ class EduVPN(object): raise location_err def renew_session(self) -> None: + """Renew the session. This invalidates the tokens and runs the necessary callbacks to log back in + + :raises WrappedError: An error by the Go library + """ renew_err = self.go_function(self.lib.RenewSession) if renew_err: raise renew_err def should_renew_button(self) -> bool: + """Whether or not the UI should show the renew button + + :return: Whether or not the return button should be shown + :rtype: bool + """ return self.go_function(self.lib.ShouldRenewButton) def in_fsm_state(self, state_id: State) -> bool: + """Check whether or not the FSM is in the provided state + + :param state_id: State: The state to check for + + :return: Whether or not the FSM is in the provided state + :rtype: bool + """ return self.go_function(self.lib.InFSMState, state_id) def get_saved_servers(self) -> Optional[List[Server]]: + """Get a list of saved servers + + :return: The list of Servers if there are any + :rtype: Optional[List[Servers]] + """ servers, servers_err = self.go_function( self.lib.GetSavedServers, decode_func=lambda lib, x: get_data_error(lib, x, get_servers), @@ -271,6 +480,15 @@ eduvpn_objects: Dict[str, EduVPN] = {} @VPNStateChange def state_callback(name: bytes, old_state: int, new_state: int, data: Any) -> None: + """The internal callback that is passed to the Go library + + :param name: bytes: The name of the client + :param old_state: int: The old state + :param new_state: int: The new state + :param data: Any: The data that still needs to be converted + + :meta: private: + """ name_decoded = name.decode() if name_decoded not in eduvpn_objects: return @@ -278,6 +496,15 @@ def state_callback(name: bytes, old_state: int, new_state: int, data: Any) -> No def add_as_global_object(eduvpn: EduVPN) -> bool: + """Add the provided parameter to the global objects lists so we can call the callback + + :param eduvpn: EduVPN: The class to add + + :meta: private: + + :return: Whether or not the object was added + :rtype: bool + """ global eduvpn_objects if eduvpn.name not in eduvpn_objects: eduvpn_objects[eduvpn.name] = eduvpn @@ -286,5 +513,11 @@ def add_as_global_object(eduvpn: EduVPN) -> bool: def remove_as_global_object(eduvpn: EduVPN) -> None: + """Remove the provided parameter from the global objects list + + :param eduvpn: EduVPN: The class to remove + + :meta: private: + """ global eduvpn_objects eduvpn_objects.pop(eduvpn.name, None) diff --git a/wrappers/python/eduvpn_common/server.py b/wrappers/python/eduvpn_common/server.py index 6df23a6..686114b 100644 --- a/wrappers/python/eduvpn_common/server.py +++ b/wrappers/python/eduvpn_common/server.py @@ -6,6 +6,12 @@ from eduvpn_common.types import cServer, cServerLocations, cServerProfiles, cSer class Profile: + """The class that represents a server profile. + + :param: identifier: str: The identifier (id) of the profile + :param: display_name: str: The display name of the profile + :param: default_gateway: str: Whether or not this profile should have the default gateway set + """ def __init__(self, identifier: str, display_name: str, default_gateway: bool): self.identifier = identifier self.display_name = display_name @@ -16,18 +22,35 @@ class Profile: class Profiles: + """The class that represents a list of profiles + + :param: profiles: List[Profile]: A list of profiles + :param: current: int: The current profile index + """ def __init__(self, profiles: List[Profile], current: int): self.profiles = profiles self.current_index = current @property def current(self) -> Optional[Profile]: + """Get the current profile if there is any + + :return: The profile if there is a current one (meaning the index is valid) + :rtype: Optional[Profile] + """ if self.current_index < len(self.profiles): return self.profiles[self.current_index] return None class Server: + """The class that represents a server. Use this for a custom server + + :param: url: str: The base URL of the server + :param: display_name: str: The display name of the server + :param: profiles: Optional[Profiles]: The profiles if there are any already obtained, defaults to None + :param: expire_time: int: The expiry time in a Unix timestamp, defaults to 0 + """ def __init__( self, url: str, @@ -45,10 +68,23 @@ class Server: @property def category(self) -> str: + """Return the category of the server as a string + + :return: The category string, "Custom Server" + :rtype: str + """ return "Custom Server" class InstituteServer(Server): + """The class that represents an Institute Access Server + + :param: url: str: The base URL of the Institute Access Server + :param: display_name: str: The display name of the Institute Access Server + :param: support_contact: List[str]: The list of support contacts + :param: profiles: Profiles: The profiles of the server + :param: expire_time: int: The expiry time in a Unix timestamp + """ def __init__( self, url: str, @@ -62,10 +98,24 @@ class InstituteServer(Server): @property def category(self) -> str: + """Return the category of the institute server as a string + + :return: The category string, "Institute Access Server" + :rtype: str + """ return "Institute Access Server" class SecureInternetServer(Server): + """The class that represents a Secure Internet Server + + :param: org_id: str: The organization ID of the Secure Internet Server as returned by Discovery + :param: display_name: str: The display name of the server + :param: support_contact: List[str]: The list of support contacts of the server + :param: profiles: Profiles: The list of profiles that the server has + :param: expire_time: int: The expiry time in a Unix timestamp + :param: country_code: str: The country code of the server + """ def __init__( self, org_id: str, @@ -82,10 +132,22 @@ class SecureInternetServer(Server): @property def category(self) -> str: + """Return the category of the secure internet server as a string + + :return: The category string, "Secure Internet Server" + :rtype: str + """ return "Secure Internet Server" def get_type_for_str(type_str: str) -> Type[Server]: + """Get the right class type for a certain string input + + :param type_str: str: The string that represents the type of server, one of secure_internet, institute_access, custom_server + + :return: The server, defaults to Institute Server if an invalid input is given + :rtype: Type[Server] + """ if type_str == "secure_internet": return SecureInternetServer if type_str == "custom_server": @@ -94,6 +156,15 @@ def get_type_for_str(type_str: str) -> Type[Server]: def get_profiles(ptr) -> Optional[Profiles]: + """Get the profiles from the Go shared library and convert it to a Python usable structure + + :param ptr: The pointer to the Profiles as returned by the Go library + + :meta: private: + + :return: Profiles if there are any + :rtype: Optional[Profiles] + """ if not ptr: return None profiles = [] @@ -115,7 +186,17 @@ def get_profiles(ptr) -> Optional[Profiles]: return Profiles(profiles, current_profile) -def get_server(ptr, _type=None) -> Optional[Server]: +def get_server(ptr, _type: Optional[str] = None) -> Optional[Server]: + """Get the server from the Go shared library and convert it to a Python usable structure + + :param ptr: The pointer as returned by the Go library + :param _type: (Default value = None): The optional parameter that represents whether or not the type is enforced to the input. If None it is automatically determined + + :meta: private: + + :return: Server if there is any + :rtype: Optional[Server] + """ if not ptr: return None @@ -154,18 +235,48 @@ def get_server(ptr, _type=None) -> Optional[Server]: def get_transition_server(lib: CDLL, ptr: c_void_p) -> Optional[Server]: + """Get a server from a transition event + + :param lib: CDLL: The Go shared library + :param ptr: c_void_p: The Go's returned C pointer that represents the Server + + :meta: private: + + :return: The server if there is any + :rtype: Optional[Server] + """ server = get_server(cast(ptr, POINTER(cServer))) lib.FreeServer(ptr) return server def get_transition_profiles(lib: CDLL, ptr: c_void_p) -> Optional[Profiles]: + """Get profiles from a transition event + + :param lib: CDLL: The Go shared library + :param ptr: c_void_p: The Go's returned C pointer that represents the profiles + + :meta: private: + + :return: The profiles if there is any + :rtype: Optional[Profiles] + """ profiles = get_profiles(cast(ptr, POINTER(cServerProfiles))) lib.FreeProfiles(ptr) return profiles def get_servers(lib: CDLL, ptr: c_void_p) -> Optional[List[Server]]: + """Get servers from the Go library as a C structure and return a Python usable structure + + :param lib: CDLL: The Go shared library + :param ptr: c_void_p: The C pointer to the servers structure + + :meta: private: + + :return: The list of Servers if there is any + :rtype: Optional[List[Server]] + """ if ptr: returned = [] servers = cast(ptr, POINTER(cServers)).contents @@ -193,6 +304,16 @@ def get_servers(lib: CDLL, ptr: c_void_p) -> Optional[List[Server]]: def get_locations(lib: CDLL, ptr: c_void_p) -> Optional[List[str]]: + """Get locations from the Go library as a C structure and return a Python usable structure + + :param lib: CDLL: The Go shared library + :param ptr: c_void_p: The C pointer to the locations structure + + :meta: private: + + :return: The list of servers if there are any + :rtype: Optional[List[str]] + """ if ptr: locations = cast(ptr, POINTER(cServerLocations)).contents location_list = [] diff --git a/wrappers/python/eduvpn_common/state.py b/wrappers/python/eduvpn_common/state.py index dfa04c9..227d3c2 100644 --- a/wrappers/python/eduvpn_common/state.py +++ b/wrappers/python/eduvpn_common/state.py @@ -2,12 +2,18 @@ from enum import IntEnum class StateType(IntEnum): + """ + The State Type enum. Wait types are mostly used for internal code + """ ENTER = 1 LEAVE = 2 WAIT = 3 class State(IntEnum): + """ + The State enum. Each state here also exists in the Go library + """ DEREGISTERED = 0 NO_SERVER = 1 ASK_LOCATION = 2 diff --git a/wrappers/python/eduvpn_common/types.py b/wrappers/python/eduvpn_common/types.py index 9f5fba0..a58adff 100644 --- a/wrappers/python/eduvpn_common/types.py +++ b/wrappers/python/eduvpn_common/types.py @@ -17,6 +17,10 @@ from eduvpn_common.error import ErrorLevel, WrappedError class cError(Structure): + """The C type that represents the Error as returned by the Go library + + :meta: private: + """ _fields_ = [ ("level", c_int), ("traceback", c_char_p), @@ -25,10 +29,15 @@ class cError(Structure): class cServerLocations(Structure): + """The C type that represents the Server Locations as returned by the Go library + + :meta: private: + """ _fields_ = [("locations", POINTER(c_char_p)), ("total_locations", c_size_t)] class cDiscoveryOrganization(Structure): + """The C type that represents a Discovery Organization as returned by the Go library""" _fields_ = [ ("display_name", c_char_p), ("org_id", c_char_p), @@ -38,6 +47,10 @@ class cDiscoveryOrganization(Structure): class cDiscoveryOrganizations(Structure): + """The C type that represents Discovery Organizations as returned by the Go library + + :meta: private: + """ _fields_ = [ ("version", c_ulonglong), ("organizations", POINTER(POINTER(cDiscoveryOrganization))), @@ -46,6 +59,10 @@ class cDiscoveryOrganizations(Structure): class cDiscoveryServer(Structure): + """The C type that represents a Discovery Server as returned by the Go library + + :meta: private: + """ _fields_ = [ ("authentication_url_template", c_char_p), ("base_url", c_char_p), @@ -61,6 +78,10 @@ class cDiscoveryServer(Structure): class cDiscoveryServers(Structure): + """The C type that represents Discovery Servers as returned by the Go library + + :meta: private: + """ _fields_ = [ ("version", c_ulonglong), ("servers", POINTER(POINTER(cDiscoveryServer))), @@ -69,6 +90,10 @@ class cDiscoveryServers(Structure): class cServerProfile(Structure): + """The C type that represents a Server Profile as returned by the Go library + + :meta: private: + """ _fields_ = [ ("identifier", c_char_p), ("display_name", c_char_p), @@ -77,6 +102,10 @@ class cServerProfile(Structure): class cServerProfiles(Structure): + """The C type that represents Server Profiles as returned by the Go library + + :meta: private: + """ _fields_ = [ ("current", c_int), ("profiles", POINTER(POINTER(cServerProfile))), @@ -85,6 +114,10 @@ class cServerProfiles(Structure): class cServer(Structure): + """The C type that represents a Server as returned by the Go library + + :meta: private: + """ _fields_ = [ ("identifier", c_char_p), ("display_name", c_char_p), @@ -98,6 +131,10 @@ class cServer(Structure): class cServers(Structure): + """The C type that represents Servers as returned by the Go library + + :meta: private: + """ _fields_ = [ ("custom_servers", POINTER(POINTER(cServer))), ("total_custom", c_size_t), @@ -108,17 +145,36 @@ class cServers(Structure): class DataError(Structure): + """The C type that represents a tuple of data and error as returned by the Go library + + :meta: private: + """ _fields_ = [("data", c_void_p), ("error", c_void_p)] class ConfigError(Structure): + """The C type that represents the data that gets by the Go library returned when a config is obtained + + :meta: private: + """ _fields_ = [("config", c_void_p), ("config_type", c_void_p), ("error", c_void_p)] +# The type for a Go state change callback VPNStateChange = CFUNCTYPE(None, c_char_p, c_int, c_int, c_void_p) def encode_args(args: List[Any], types: List[Any]) -> Iterator[Any]: + """Encode the arguments ready to be used by the Go library + + :param args: List[Any]: The list of arguments + :param types: List[Any]: The list of the types of the arguments + + :meta: private: + + :return: The arg generator + :rtype: Iterator[Any] + """ for arg, t in zip(args, types): # c_char_p needs the str to be encoded to bytes if t is c_char_p: @@ -126,7 +182,16 @@ def encode_args(args: List[Any], types: List[Any]) -> Iterator[Any]: yield arg -def decode_res(res: Any): +def decode_res(res: Any) -> Any: + """Decode a result as obtained by the Go library + + :param res: Any: The result + + :meta: private: + + :return: The argument decoded + :rtype: Any + """ decode_map = { c_int: get_bool, c_void_p: get_error, @@ -137,6 +202,17 @@ def decode_res(res: Any): def get_ptr_string(lib: CDLL, ptr: c_void_p) -> str: + """Convert a C string pointer to a Python usable string. + This makes sure to free all memory allocated by the Go library + + :param lib: CDLL: The Go shared library + :param ptr: c_void_p: The pointer to the C string + + :meta: private: + + :return: The string converted to Python + :rtype: str + """ if ptr: string = cast(ptr, c_char_p).value lib.FreeString(ptr) @@ -146,6 +222,18 @@ def get_ptr_string(lib: CDLL, ptr: c_void_p) -> str: def get_ptr_list_strings(lib: CDLL, strings: pointer, total_strings: int) -> List[str]: + """Convert a list of C strings to a Python usable list of strings + This list is not freed here but is later freed in the Go library for convenience + + :param lib: CDLL: The Go shared library + :param strings: pointer: The C pointer to the strings list + :param total_strings: int: The total strings in the list + + :meta: private: + + :return: The list of strings converted to Python + :rtype: List[str] + """ if strings: strings_list = [] for i in range(total_strings): @@ -155,6 +243,16 @@ def get_ptr_list_strings(lib: CDLL, strings: pointer, total_strings: int) -> Lis def get_error(lib: CDLL, ptr: c_void_p) -> Optional[WrappedError]: + """Convert a C error structure to a Python usable error structure + + :param lib: CDLL: The Go shared library + :param ptr: c_void_p: The pointer to the C error struct + + :meta: private: + + :return: The error if there is one + :rtype: Optional[WrappedError] + """ if not ptr: return None err = cast(ptr, POINTER(cError)).contents @@ -168,6 +266,16 @@ def get_error(lib: CDLL, ptr: c_void_p) -> Optional[WrappedError]: def get_config_error( lib: CDLL, config_error: ConfigError ) -> Tuple[str, str, Optional[WrappedError]]: + """Convert a C config structure to a Python usable config structure + + :param lib: CDLL: The Go shared library + :param config_error: ConfigError: The config error structure + + :meta: private: + + :return: The configuration, configuration type ('openvpn'/'wireguard') and an optional error + :rtype: Tuple[str, str, Optional[WrappedError]] + """ config = get_ptr_string(lib, config_error.config) config_type = get_ptr_string(lib, config_error.config_type) err = get_error(lib, config_error.error) @@ -176,11 +284,32 @@ def get_config_error( def get_data_error( lib: CDLL, data_error: DataError, data_conv: Callable = get_ptr_string -) -> Tuple[str, Optional[WrappedError]]: +) -> Tuple[Any, Optional[WrappedError]]: + """Convert a C data+error structure to a Python usable data+error structure + + :param lib: CDLL: The Go shared library + :param data_error: DataError: The data error C structure + :param data_conv: Callable: (Default value = get_ptr_string): The function that converts the data part + + :meta: private: + + :return: The data and optional error + :rtype: Tuple[Any, Optional[WrappedError]] + """ data = data_conv(lib, data_error.data) error = get_error(lib, data_error.error) return data, error def get_bool(lib: CDLL, boolInt: c_int) -> bool: + """Get a bool from the Go shared library. Essentially just checking if an int represents 'True' + + :param lib: CDLL: The Go shared library + :param boolInt: c_int: The C integer that needs to be converted to the Python bool + + :meta: private: + + :return: The boolean converted to Python + :rtype: bool + """ return boolInt == 1 |
