def get_profile_config(oauth, api_base_uri, profile_id): # type: (OAuth2Session, str, str) -> str """ Return a profile configuration args: oauth (requests_oauthlib.OAuth2Session): oauth2 object api_base_uri (str): the instance base URI profile_id (str): """ logger.info("Retrieving profile config from {}".format(api_base_uri)) try: response = oauth.get( api_base_uri + '/profile_config?profile_id={}'.format(profile_id)) except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: raise EduvpnException("can't create profile, error code {}".format( response.status_code)) # note: this is a bit ambiguous, in case there is an error, the result is json, otherwise clear text. try: json = response.json()['profile_config'] except Exception: # probably valid response return response.text else: if not json['ok']: raise EduvpnException(json['error']) else: raise EduvpnException( "Server error! No profile config returned but no error also.")
def two_factor_enroll_yubi(oauth, api_base_uri, yubi_key_otp): # type : (OAuth2Session, str, str) -> None try: response = oauth.post(api_base_uri + '/two_factor_enroll_yubi', data={'yubi_key_otp': yubi_key_otp}) except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: raise EduvpnException("can't retrieve user info, error code {}".format(response.status_code)) data = response.json()['two_factor_enroll_yubi'] if not data['ok']: raise EduvpnException(data['error'])
def create_keypair( oauth, api_base_uri): # type: (OAuth2Session, str) -> Tuple[str, str] """ Create remote keypair and return results args: oauth (requests_oauthlib.OAuth2Session): oauth2 object api_base_uri (str): the instance base URI returns: tuple(str, str): certificate and key """ logger.info( "Creating and retrieving key pair from {}".format(api_base_uri)) try: response = oauth.post(api_base_uri + '/create_keypair', data={'display_name': 'eduVPN for Linux'}) except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: raise EduvpnException("can't create keypair, error code {}".format( response.status_code)) keypair = response.json()['create_keypair']['data'] cert = keypair['certificate'] key = keypair['private_key'] return cert, key
def user_messages(oauth, api_base_uri): # type: (OAuth2Session, str) -> Any """ These are messages specific to the user. It can contain a message about the user being blocked, or other personal messages from the VPN administrator. args: oauth (requests_oauthlib.OAuth2Session): oauth2 object api_base_uri (str): the instance base URI returns: list: a list of dicts with date_time, message, type keys """ logger.info("Retrieving user messages from {}".format(api_base_uri)) try: response = oauth.get(api_base_uri + '/user_messages') except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: raise EduvpnException( "can't fetch user messages, error code {}".format( response.status_code)) messages = response.json()['user_messages'] _ = messages['ok'] data = messages['data'] for d in data: yield dateutil.parser.parse(d['date_time']), d['type'], d['message']
def create_config(oauth, api_base_uri, display_name, profile_id): # type: (OAuth2Session, str, str, str) -> str """ Create a configuration for a given profile. args: oauth (requests_oauthlib.OAuth2Session): oauth2 object api_base_uri (str): the instance base URI display_name (str): profile_id (str): """ logger.info("Creating config with name '{}' and profile '{}' at {}".format( display_name, profile_id, api_base_uri)) try: response = oauth.post(api_base_uri + '/create_config', data={ 'display_name': display_name, 'profile_id': profile_id }) except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: raise EduvpnException("can't create config, error code {}".format( response.status_code)) return response.json()
def disconnect_provider(uuid): """ Disconnect the network manager configuration by its UUID args: uuid (str): the unique ID of the configuration """ logger.info("Disconnecting profile with uuid {} using NetworkManager".format(uuid)) if not have_dbus(): raise EduvpnException("No DBus daemon running") conns = [i for i in NetworkManager.NetworkManager.ActiveConnections if i.Uuid == uuid] if len(conns) == 0: raise EduvpnException("no active connection found with uuid {}".format(uuid)) for conn in conns: NetworkManager.NetworkManager.DeactivateConnection(conn)
def connect_provider(uuid): """ Enable the network manager configuration by its UUID args: uuid (str): the unique ID of the configuration """ logger.info("connecting profile with uuid {} using NetworkManager".format(uuid)) if not have_dbus(): raise EduvpnException("No DBus daemon running") try: connection = NetworkManager.Settings.GetConnectionByUuid(uuid) return NetworkManager.NetworkManager.ActivateConnection(connection, "/", "/") except DBusException as e: raise EduvpnException(e)
def user_info(oauth, api_base_uri): # type: (OAuth2Session, str) -> dict """ returns the user information args: oauth (requests_oauthlib.OAuth2Session): oauth2 object api_base_uri (str): the instance base URI """ logger.info("Retrieving user info from {}".format(api_base_uri)) try: response = oauth.get(api_base_uri + '/user_info') except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: raise EduvpnException("can't retrieve user info, error code {}".format( response.status_code)) data = response.json()['user_info']['data'] assert ("is_disabled" in data) assert ("two_factor_enrolled" in data) if data["two_factor_enrolled"]: assert ("two_factor_enrolled_with" in data) assert ("user_id" in data) return data
def _background(oauth, meta, builder, dialog, lets_connect): # type: (str, Metadata, Gtk.builder, Any, bool) -> None try: profiles = list_profiles(oauth, meta.api_base_uri) logger.info("There are {} profiles on {}".format( len(profiles), meta.api_base_uri)) if len(profiles) > 1: GLib.idle_add(lambda: dialog.hide()) GLib.idle_add( lambda: _select_profile_step(builder=builder, profiles=profiles, meta=meta, oauth=oauth, lets_connect=lets_connect)) elif len(profiles) == 1: _parse_choice(builder, meta, oauth, profiles[0], lets_connect=lets_connect) else: raise EduvpnException( "Either there are no VPN profiles defined, or this account does not have the " "required permissions to create a new VPN configurations for any of the " "available profiles.") except Exception as e: error = str(e) GLib.idle_add( lambda: error_helper(dialog, "Can't fetch profile list", error)) GLib.idle_add(lambda: dialog.hide()) raise
def detect_distro(release_file='/etc/os-release'): # type: (str) -> Tuple[str, str] params = {} if not os.access(release_file, os.R_OK): raise EduvpnException("Can't detect distribution version, '/etc/os-release' doesn't exist.") with open(release_file, 'r') as f: for line in f.readlines(): splitted = line.strip().split('=') if len(splitted) == 2: key, value = splitted params[key] = value.strip('""') if 'ID' not in params and 'VERSION_ID' not in params: raise EduvpnException("Can't detect distribution version, '/etc/os-release' doesn't " "contain ID and VERSION_ID fields") return params['ID'], params['VERSION_ID']
def two_factor_enroll_totp(oauth, api_base_uri, secret, key): # type : (OAuth2Session, str, secret, str) -> None prefix = '/two_factor_enroll_totp' url = api_base_uri + prefix logger.info("2fa totp enroling on {} with secret={} and key={}".format(url, secret, key)) try: response = oauth.post(url, data={'totp_secret': secret, 'totp_key': key}) except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: logger.error(str(response.content)) raise EduvpnException("can't enable 2fa otp, error code {} response {}".format(response.status_code, response.content)) data = response.json()['two_factor_enroll_totp'] if not data['ok']: raise EduvpnException(data['error'])
def check_certificate(oauth, api_base_uri, common_name): # type : (OAuth2Session, str, common_name) -> dict prefix = '/check_certificate' url = api_base_uri + prefix logger.info("checking client certificate on {} with common_name={}".format(url, common_name)) try: response = oauth.get("{}?common_name={}".format(url, common_name)) except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: logger.error(str(response.content)) raise EduvpnException("can't check client certificate, error code {} response {}".format(response.status_code, response.content)) parsed = response.json()['check_certificate'] if not parsed['ok']: raise EduvpnException(parsed['error']) return parsed['data']
def write(self): if not self.uuid: raise EduvpnException('uuid field not set') fields = [f for f in dir(self) if not f.startswith('_') and not callable(getattr(self, f))] d = {field: getattr(self, field) for field in fields} p = path.join(providers_path, self.uuid + '.json') logger.info("storing metadata in {}".format(p)) serialized = json.dumps(d) mkdir_p(providers_path) with open(p, 'w') as f: f.write(serialized)
def _cert_check(meta, oauth, builder, info): common_name = common_name_from_cert(meta.cert.encode('ascii')) cert_valid = check_certificate(oauth, meta.api_base_uri, common_name) if not cert_valid['is_valid']: logger.warning('client certificate not valid, reason: {}'.format( cert_valid['reason'])) if cert_valid['reason'] in ('certificate_missing', 'certificate_not_yet_valid', 'certificate_expired'): logger.info('Going to try to fetch new keypair') cert, key = create_keypair(oauth, meta.api_base_uri) update_keys_provider(meta.uuid, cert, key) elif cert_valid['reason'] == 'user_disabled': raise EduvpnException('Your account has been disabled.') else: raise EduvpnException( 'Your client certificate is invalid ({})'.format( cert_valid['reason'])) _fetch_updated_config(oauth, meta, builder, info)
def ovpn_to_nm(config, meta, display_name, username=None): # type: (dict, Metadata, str, Optional[str]) -> object """Generate a NetworkManager style config dict from a parsed ovpn config dict.""" logger.info("generating config for {} ({})".format(display_name, meta.uuid)) settings = {'connection': {'id': display_name, 'type': 'vpn', 'uuid': meta.uuid}, 'ipv4': {'method': 'auto'}, 'ipv6': {'method': 'auto'}, 'vpn': {'data': {'auth': config.get('auth', 'SHA256'), 'cipher': config.get('cipher', 'AES-256-CBC'), 'connection-type': config.get('connection-type', 'tls'), 'dev': 'tun', 'remote': ",".join(":".join(r) for r in config['remote']), 'remote-cert-tls': 'server', # 'tls-cipher' is not supported on older nm (like ubuntu 16.04) # 'tls-cipher': config.get('tls-cipher', 'TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384') }, 'service-type': 'org.freedesktop.NetworkManager.openvpn'} } # issue #138, not supported by older network-manager-openvpn # if 'server-poll-timeout' in config: # settings['vpn']['data']['connect-timeout'] = config['server-poll-timeout'] if 'comp-lzo' in config: settings['vpn']['data']['comp-lzo'] = config['comp-lzo'] or 'adaptive' # 2 factor auth enabled if 'auth-user-pass' in config: if not username: raise EduvpnException("You need to enroll for 2FA in the user portal " "first before being able to connect to this profile.") logger.info("looks like 2 factor authentication is enabled, enabling this in NM config") settings['vpn']['data']['cert-pass-flags'] = '0' settings['vpn']['data']['connection-type'] = 'password-tls' settings['vpn']['data']['password-flags'] = '2' settings['vpn']['data']['username'] = username if 'ca' in config: ca_path = write_cert(config.get('ca'), 'ca', meta.uuid) settings['vpn']['data']['ca'] = ca_path if 'tls-auth' in config: settings['vpn']['data']['ta'] = write_cert(config.get('tls-auth'), 'ta', meta.uuid) settings['vpn']['data']['ta-dir'] = config.get('key-direction', '1') elif 'tls-crypt' in config: settings['vpn']['data']['tls-crypt'] = write_cert(config.get('tls-crypt'), 'tc', meta.uuid) else: logging.info("'tls-crypt' and 'tls-auth' not found in configuration returned by server") return settings
def _background(oauth, meta, builder, dialog): try: profiles = list_profiles(oauth, meta.api_base_uri) logger.info("There are {} profiles on {}".format(len(profiles), meta.api_base_uri)) if len(profiles) > 1: GLib.idle_add(lambda: dialog.hide()) GLib.idle_add(lambda: select_profile_step(builder=builder, profiles=profiles, meta=meta, oauth=oauth)) elif len(profiles) == 1: meta.profile_display_name, meta.profile_id, meta.two_factor = profiles[0] two_auth_step(builder=builder, oauth=oauth, meta=meta) else: raise EduvpnException("Either there are no VPN profiles defined, or this account does not have the " "required permissions to create a new VPN configurations for any of the " "available profiles.") except Exception as e: GLib.idle_add(lambda: error_helper(dialog, "Can't fetch profile list", str(e))) GLib.idle_add(lambda: dialog.hide()) raise
def delete_provider(uuid): """ Delete the network manager configuration by its UUID args: uuid (str): the unique ID of the configuration """ metadata = os.path.join(providers_path, uuid + '.json') logger.info("deleting metadata file {}".format(metadata)) try: os.remove(metadata) except Exception as e: logger.error("can't remove ovpn file: {}".format(str(e))) if not have_dbus(): return logger.info("deleting profile with uuid {} using NetworkManager".format(uuid)) all_connections = NetworkManager.Settings.ListConnections() conns = [c for c in all_connections if c.GetSettings()['connection']['uuid'] == uuid] if len(conns) != 1: raise EduvpnException("{} connections matching uid {}".format(len(conns), uuid)) conn = conns[0] logger.info("removing certificates for {}".format(uuid)) for f in ['ca', 'cert', 'key', 'ta']: if f not in conn.GetSettings()['vpn']['data']: logger.error("key {} not in config for {}".format(f, uuid)) continue path = conn.GetSettings()['vpn']['data'][f] logger.info("removing certificate {}".format(path)) try: os.remove(path) except (IOError, OSError) as e: logger.error("can't remove certificate {}: {}".format(path, e)) try: conn.Delete() except Exception as e: logger.error("can't remove networkmanager connection: {}".format(str(e))) raise
def system_messages(oauth, api_base_uri): """ Return all system messages args: oauth (requests_oauthlib.OAuth2Session): oauth2 object api_base_uri (str): the instance base URI """ logger.info("Retrieving system messages from {}".format(api_base_uri)) try: response = oauth.get(api_base_uri + '/system_messages') except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: raise EduvpnException("can't fetch system messages, error code {}".format(response.status_code)) messages = response.json()['system_messages'] _ = messages['ok'] data = messages['data'] for d in data: yield dateutil.parser.parse(d['date_time']), d['type'], d['message']
def list_profiles(oauth, api_base_uri): # type (OAuth2Session, str) -> dict """ List profiles on instance args: oauth (requests_oauthlib.OAuth2Session): oauth2 object api_base_uri (str): the instance base URI returns: list: of available profiles on the instance (display_name, profile_id, two_factor) """ logger.info("Retrieving profile list from {}".format(api_base_uri)) try: response = oauth.get(api_base_uri + '/profile_list') except InvalidGrantError as e: raise EduvpnAuthException(str(e)) if response.status_code == 401: raise EduvpnAuthException("request returned error 401") elif response.status_code != 200: raise EduvpnException("can't list profiles, error code {}".format( response.status_code)) data = response.json()['profile_list']['data'] profiles = [] for profile in data: display_name = translate_display_name(profile["display_name"]) profile_id = profile["profile_id"] two_factor = profile["two_factor"] if two_factor: if "two_factor_method" in profile: two_factor_method = profile["two_factor_method"] else: two_factor_method = ["yubi", "totp"] else: two_factor_method = [] # we load this into a GtkListModel which doesnt support lists, so we concat them with , profiles.append((display_name, profile_id, two_factor, ",".join(two_factor_method))) return profiles