eduvpn-client 4.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eduvpn/__init__.py +1 -0
- eduvpn/__main__.py +4 -0
- eduvpn/app.py +628 -0
- eduvpn/cli.py +720 -0
- eduvpn/config.py +68 -0
- eduvpn/connection.py +244 -0
- eduvpn/discovery.py +95 -0
- eduvpn/i18n.py +104 -0
- eduvpn/keyring.py +155 -0
- eduvpn/nm.py +916 -0
- eduvpn/notify.py +34 -0
- eduvpn/ovpn.py +99 -0
- eduvpn/server.py +313 -0
- eduvpn/settings.py +29 -0
- eduvpn/storage.py +71 -0
- eduvpn/utils.py +253 -0
- eduvpn/variants.py +72 -0
- eduvpn_client-4.3.0.dist-info/LICENSE +674 -0
- eduvpn_client-4.3.0.dist-info/METADATA +51 -0
- eduvpn_client-4.3.0.dist-info/RECORD +23 -0
- eduvpn_client-4.3.0.dist-info/WHEEL +5 -0
- eduvpn_client-4.3.0.dist-info/entry_points.txt +7 -0
- eduvpn_client-4.3.0.dist-info/top_level.txt +1 -0
eduvpn/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.3.0"
|
eduvpn/__main__.py
ADDED
eduvpn/app.py
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import time
|
|
6
|
+
import webbrowser
|
|
7
|
+
from typing import Any, Callable, Iterator, Optional, TextIO
|
|
8
|
+
|
|
9
|
+
from eduvpn_common.main import EduVPN, ServerType, WrappedError
|
|
10
|
+
from eduvpn_common.state import State, StateType
|
|
11
|
+
from eduvpn_common.types import ProxyReady, ProxySetup, ReadRxBytes
|
|
12
|
+
|
|
13
|
+
from eduvpn import nm
|
|
14
|
+
from eduvpn.config import Configuration
|
|
15
|
+
from eduvpn.connection import Config, Connection, Protocol, parse_config, parse_expiry, parse_tokens
|
|
16
|
+
from eduvpn.keyring import DBusKeyring, InsecureFileKeyring, TokenKeyring
|
|
17
|
+
from eduvpn.server import ServerDatabase, parse_profiles, parse_required_transition
|
|
18
|
+
from eduvpn.utils import (
|
|
19
|
+
handle_exception,
|
|
20
|
+
model_transition,
|
|
21
|
+
run_in_background_thread,
|
|
22
|
+
run_in_glib_thread,
|
|
23
|
+
set_failovered,
|
|
24
|
+
set_online_detecting,
|
|
25
|
+
)
|
|
26
|
+
from eduvpn.variants import ApplicationVariant
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ApplicationModelTransitions:
|
|
32
|
+
def __init__(self, common: EduVPN, variant: ApplicationVariant) -> None:
|
|
33
|
+
self.common = common
|
|
34
|
+
self.common.register_class_callbacks(self)
|
|
35
|
+
self.server_db = ServerDatabase(common, variant.use_predefined_servers)
|
|
36
|
+
|
|
37
|
+
@model_transition(State.MAIN, StateType.ENTER)
|
|
38
|
+
def get_previous_servers(self, old_state: State, data):
|
|
39
|
+
logger.debug(f"Transition: MAIN, old state: {old_state}")
|
|
40
|
+
return self.server_db.configured
|
|
41
|
+
|
|
42
|
+
@model_transition(State.DEREGISTERED, StateType.ENTER)
|
|
43
|
+
def deregistered(self, old_state: State, data: str):
|
|
44
|
+
logger.debug(f"Transition: DEREGISTERED, old state: {old_state}")
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
@model_transition(State.ADDING_SERVER, StateType.ENTER)
|
|
48
|
+
def adding_server(self, old_state: State, data: str):
|
|
49
|
+
logger.debug(f"Transition: ADDING_SERVER, old state: {old_state}")
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
@model_transition(State.GETTING_CONFIG, StateType.ENTER)
|
|
53
|
+
def getting_config(self, old_state: State, data: str):
|
|
54
|
+
logger.debug(f"Transition: GETTING_CONFIG, old state: {old_state}")
|
|
55
|
+
return data
|
|
56
|
+
|
|
57
|
+
@model_transition(State.DISCONNECTED, StateType.ENTER)
|
|
58
|
+
def disconnected_server(self, old_state: State, data: str):
|
|
59
|
+
logger.debug(f"Transition: DISCONNECTED, old state: {old_state}")
|
|
60
|
+
return self.server_db.current
|
|
61
|
+
|
|
62
|
+
@model_transition(State.DISCONNECTING, StateType.ENTER)
|
|
63
|
+
def disconnecting(self, old_state: State, data):
|
|
64
|
+
logger.debug(f"Transition: DISCONNECTING, old state: {old_state}")
|
|
65
|
+
return self.server_db.current
|
|
66
|
+
|
|
67
|
+
@model_transition(State.ASK_PROFILE, StateType.ENTER)
|
|
68
|
+
def ask_profile(self, old_state: State, data: str):
|
|
69
|
+
logger.debug(f"Transition: ASK_PROFILE, old state: {old_state}")
|
|
70
|
+
cookie, profiles = parse_required_transition(data, get=parse_profiles)
|
|
71
|
+
|
|
72
|
+
def set_profile(prof):
|
|
73
|
+
self.common.cookie_reply(cookie, prof)
|
|
74
|
+
|
|
75
|
+
return (set_profile, profiles)
|
|
76
|
+
|
|
77
|
+
@model_transition(State.ASK_LOCATION, StateType.ENTER)
|
|
78
|
+
def ask_location(self, old_state: State, data):
|
|
79
|
+
cookie, locations = parse_required_transition(data)
|
|
80
|
+
|
|
81
|
+
def set_location(loc):
|
|
82
|
+
self.common.cookie_reply(cookie, loc)
|
|
83
|
+
|
|
84
|
+
return (set_location, locations)
|
|
85
|
+
|
|
86
|
+
@model_transition(State.OAUTH_STARTED, StateType.ENTER)
|
|
87
|
+
def start_oauth(self, old_state: State, url: str):
|
|
88
|
+
logger.debug(f"Transition: OAUTH_STARTED, old state: {old_state}")
|
|
89
|
+
self.open_browser(url)
|
|
90
|
+
return url
|
|
91
|
+
|
|
92
|
+
@model_transition(State.GOT_CONFIG, StateType.ENTER)
|
|
93
|
+
def parse_config(self, old_state: State, data):
|
|
94
|
+
logger.debug(f"Transition: GOT_CONFIG, old state: {old_state}")
|
|
95
|
+
return self.server_db.current
|
|
96
|
+
|
|
97
|
+
@run_in_background_thread("open-browser")
|
|
98
|
+
def open_browser(self, url):
|
|
99
|
+
logger.debug(f"Opening web browser with url: {url}")
|
|
100
|
+
webbrowser.open(url)
|
|
101
|
+
# Explicitly wait to not have zombie processes
|
|
102
|
+
# See https://bugs.python.org/issue5993
|
|
103
|
+
logger.debug("Running os.wait for browser")
|
|
104
|
+
try:
|
|
105
|
+
os.wait()
|
|
106
|
+
except ChildProcessError:
|
|
107
|
+
pass
|
|
108
|
+
logger.debug("Done waiting for browser")
|
|
109
|
+
|
|
110
|
+
@model_transition(State.CONNECTED, StateType.ENTER)
|
|
111
|
+
def parse_connected(self, old_state: State, data):
|
|
112
|
+
logger.debug(f"Transition: CONNECTED, old state: {old_state}")
|
|
113
|
+
server = self.server_db.current
|
|
114
|
+
expire_times = parse_expiry(self.common.get_expiry_times())
|
|
115
|
+
return (server, expire_times)
|
|
116
|
+
|
|
117
|
+
@model_transition(State.CONNECTING, StateType.ENTER)
|
|
118
|
+
def parse_connecting(self, old_state: State, data):
|
|
119
|
+
logger.debug(f"Transition: CONNECTING, old state: {old_state}")
|
|
120
|
+
return self.server_db.current
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ApplicationModel:
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
common: EduVPN,
|
|
127
|
+
config,
|
|
128
|
+
variant: ApplicationVariant,
|
|
129
|
+
nm_manager,
|
|
130
|
+
) -> None:
|
|
131
|
+
self.common = common
|
|
132
|
+
self.config = config
|
|
133
|
+
self._keyring: TokenKeyring | None = None
|
|
134
|
+
self.transitions = ApplicationModelTransitions(common, variant)
|
|
135
|
+
self.variant = variant
|
|
136
|
+
self.nm_manager = nm_manager
|
|
137
|
+
self._was_tcp = False
|
|
138
|
+
self._should_failover = False
|
|
139
|
+
self._peer_ips_proxy = None
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def keyring(self):
|
|
143
|
+
if self._keyring is not None:
|
|
144
|
+
return self._keyring
|
|
145
|
+
keyring = DBusKeyring(self.variant)
|
|
146
|
+
if not keyring.available:
|
|
147
|
+
logger.warning("Secure keyring not available, reverting to insecure file keyring!")
|
|
148
|
+
keyring = InsecureFileKeyring(self.variant)
|
|
149
|
+
self._keyring = keyring
|
|
150
|
+
return self._keyring
|
|
151
|
+
|
|
152
|
+
def register(self, debug: bool):
|
|
153
|
+
self.common.register(debug=debug)
|
|
154
|
+
self.common.set_token_handler(self.load_tokens, self.save_tokens)
|
|
155
|
+
|
|
156
|
+
def cancel(self):
|
|
157
|
+
# Cancel any eduvpn-common operation
|
|
158
|
+
self.common.cancel()
|
|
159
|
+
|
|
160
|
+
# Cancel any NetworkManager operation
|
|
161
|
+
self.nm_manager.cancel()
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def server_db(self):
|
|
165
|
+
return self.transitions.server_db
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def current_server(self):
|
|
169
|
+
return self.server_db.current
|
|
170
|
+
|
|
171
|
+
def get_failover_rx(self, filehandler: Optional[TextIO]) -> int:
|
|
172
|
+
rx_bytes = self.nm_manager.get_stats_bytes(filehandler)
|
|
173
|
+
if rx_bytes is None:
|
|
174
|
+
return -1
|
|
175
|
+
return rx_bytes
|
|
176
|
+
|
|
177
|
+
def should_failover(self):
|
|
178
|
+
if self._should_failover:
|
|
179
|
+
logger.debug("eduvpn-common reports we should failover")
|
|
180
|
+
|
|
181
|
+
if self._was_tcp:
|
|
182
|
+
logger.debug("TCP was not previously triggered, failover should not continue")
|
|
183
|
+
return False
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
logger.debug("Failover should not continue")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def reconnect_tcp(self, callback: Callable):
|
|
190
|
+
def on_reconnected(success: bool):
|
|
191
|
+
callback(success)
|
|
192
|
+
|
|
193
|
+
self.reconnect(on_reconnected, prefer_tcp=True)
|
|
194
|
+
|
|
195
|
+
@run_in_background_thread("start-failover")
|
|
196
|
+
def start_failover(self, callback: Callable):
|
|
197
|
+
try:
|
|
198
|
+
rx_bytes_file = self.nm_manager.open_stats_file("rx_bytes")
|
|
199
|
+
if rx_bytes_file is None:
|
|
200
|
+
logger.error("Failed to initialize failover, failed to open rx bytes file")
|
|
201
|
+
callback(False)
|
|
202
|
+
return
|
|
203
|
+
endpoint = self.nm_manager.failover_endpoint_ip
|
|
204
|
+
if endpoint is None:
|
|
205
|
+
logger.error("Failed to initialize failover, failed to get endpoint")
|
|
206
|
+
callback(False)
|
|
207
|
+
return
|
|
208
|
+
mtu = self.nm_manager.mtu
|
|
209
|
+
if mtu is None:
|
|
210
|
+
logger.debug("failed to get MTU for failover, setting MTU to 1000")
|
|
211
|
+
mtu = 1000
|
|
212
|
+
logger.debug(
|
|
213
|
+
f"starting failover with gateway {endpoint} and MTU {mtu} for protocol {self.nm_manager.protocol}"
|
|
214
|
+
)
|
|
215
|
+
failover_delay = float(os.getenv("EDUVPN_FAILOVER_DELAY", 1))
|
|
216
|
+
logger.debug(f"Sleeping for {failover_delay}s to begin failover")
|
|
217
|
+
time.sleep(failover_delay)
|
|
218
|
+
dropped = self.common.start_failover(
|
|
219
|
+
endpoint,
|
|
220
|
+
mtu,
|
|
221
|
+
ReadRxBytes(lambda: self.get_failover_rx(rx_bytes_file)),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if dropped:
|
|
225
|
+
logger.debug("Failover exited, connection is dropped")
|
|
226
|
+
callback(True)
|
|
227
|
+
else:
|
|
228
|
+
logger.debug("Failover exited, connection is NOT dropped")
|
|
229
|
+
callback(False)
|
|
230
|
+
except WrappedError as e:
|
|
231
|
+
logger.debug(f"Failed failover, error: {e}")
|
|
232
|
+
callback(False)
|
|
233
|
+
|
|
234
|
+
def change_secure_location(self, country_code: str):
|
|
235
|
+
server = self.server_db.secure_internet
|
|
236
|
+
if server.country_code == country_code:
|
|
237
|
+
return
|
|
238
|
+
self.common.set_secure_location(server.org_id, country_code)
|
|
239
|
+
self.common.set_state(State.MAIN)
|
|
240
|
+
|
|
241
|
+
def go_back(self):
|
|
242
|
+
self.cancel()
|
|
243
|
+
self.common.set_state(State.MAIN)
|
|
244
|
+
|
|
245
|
+
def add(self, server, callback=None):
|
|
246
|
+
self.common.add_server(server.category_id, server.identifier)
|
|
247
|
+
if callback:
|
|
248
|
+
callback(server)
|
|
249
|
+
|
|
250
|
+
def remove(self, server):
|
|
251
|
+
self.common.remove_server(server.category_id, server.identifier)
|
|
252
|
+
# Delete tokens from the keyring
|
|
253
|
+
self.clear_tokens(server.category_id, server.identifier)
|
|
254
|
+
self.common.set_state(State.MAIN)
|
|
255
|
+
|
|
256
|
+
def connect_get_config(self, server, prefer_tcp: bool = False) -> Config:
|
|
257
|
+
# We prefer TCP if the user has set it or UDP is determined to be blocked
|
|
258
|
+
config = self.common.get_config(server.category_id, server.identifier, prefer_tcp)
|
|
259
|
+
return parse_config(config)
|
|
260
|
+
|
|
261
|
+
def clear_tokens(self, server_type: int, server_id: str):
|
|
262
|
+
attributes = {
|
|
263
|
+
"server": server_id,
|
|
264
|
+
"category": str(ServerType(server_type)),
|
|
265
|
+
}
|
|
266
|
+
try:
|
|
267
|
+
cleared = self.keyring.clear(attributes)
|
|
268
|
+
if not cleared:
|
|
269
|
+
logger.debug("Tokens were not cleared")
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.debug("Failed clearing tokens with exception")
|
|
272
|
+
logger.debug(e, exc_info=True)
|
|
273
|
+
|
|
274
|
+
def load_tokens(self, server_id: str, server_type: int) -> Optional[str]:
|
|
275
|
+
attributes = {"server": server_id, "category": str(ServerType(server_type))}
|
|
276
|
+
try:
|
|
277
|
+
tokens_json = self.keyring.load(attributes)
|
|
278
|
+
if tokens_json is None:
|
|
279
|
+
logger.debug("No tokens available")
|
|
280
|
+
return None
|
|
281
|
+
tokens = json.loads(tokens_json)
|
|
282
|
+
expires = tokens.get("expires_at", None)
|
|
283
|
+
if expires is None:
|
|
284
|
+
expires = tokens.get("expires", None)
|
|
285
|
+
if expires is None:
|
|
286
|
+
logger.warning("failed to parse expires")
|
|
287
|
+
return None
|
|
288
|
+
d = {
|
|
289
|
+
"access_token": tokens["access"],
|
|
290
|
+
"refresh_token": tokens["refresh"],
|
|
291
|
+
"expires_at": int(expires),
|
|
292
|
+
}
|
|
293
|
+
return json.dumps(d)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.debug("Failed loading tokens with exception:")
|
|
296
|
+
logger.debug(e, exc_info=True)
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
def save_tokens(self, server_id: str, server_type: int, tokens: str):
|
|
300
|
+
tokens_parsed = parse_tokens(tokens)
|
|
301
|
+
if tokens is None or (tokens_parsed.access == "" and tokens_parsed.refresh == ""):
|
|
302
|
+
logger.warning("Got empty tokens, not saving them to the keyring")
|
|
303
|
+
return
|
|
304
|
+
tokens_dict = {}
|
|
305
|
+
tokens_dict["access"] = tokens_parsed.access
|
|
306
|
+
tokens_dict["refresh"] = tokens_parsed.refresh
|
|
307
|
+
tokens_dict["expires_at"] = str(tokens_parsed.expires)
|
|
308
|
+
attributes = {"server": server_id, "category": str(ServerType(server_type))}
|
|
309
|
+
label = f"{server_id} - OAuth Tokens"
|
|
310
|
+
try:
|
|
311
|
+
self.keyring.save(label, attributes, json.dumps(tokens_dict))
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error("Failed saving tokens with exception:")
|
|
314
|
+
logger.error(e, exc_info=True)
|
|
315
|
+
|
|
316
|
+
def on_proxy_setup(self, fd, peer_ips):
|
|
317
|
+
logger.debug(f"got proxy fd: {fd}, peer_ips: {peer_ips}")
|
|
318
|
+
self._peer_ips_proxy = json.loads(peer_ips)
|
|
319
|
+
|
|
320
|
+
@run_in_background_thread("start-proxy")
|
|
321
|
+
def start_proxy(self, proxy, callback):
|
|
322
|
+
try:
|
|
323
|
+
self.common.start_proxyguard(
|
|
324
|
+
proxy.listen,
|
|
325
|
+
proxy.source_port,
|
|
326
|
+
proxy.peer,
|
|
327
|
+
ProxySetup(self.on_proxy_setup),
|
|
328
|
+
ProxyReady(lambda: callback()),
|
|
329
|
+
)
|
|
330
|
+
except Exception as e:
|
|
331
|
+
handle_exception(self.common, e)
|
|
332
|
+
|
|
333
|
+
def connect(
|
|
334
|
+
self,
|
|
335
|
+
server,
|
|
336
|
+
callback: Optional[Callable] = None,
|
|
337
|
+
prefer_tcp: bool = False,
|
|
338
|
+
) -> None:
|
|
339
|
+
# Variable to be used as a last resort or for debugging
|
|
340
|
+
# to override the prefer TCP setting
|
|
341
|
+
if os.environ.get("EDUVPN_PREFER_TCP", "0") == "1":
|
|
342
|
+
prefer_tcp = True
|
|
343
|
+
config = self.connect_get_config(server, prefer_tcp=prefer_tcp)
|
|
344
|
+
if not config:
|
|
345
|
+
logger.warning("no configuration available")
|
|
346
|
+
if callback:
|
|
347
|
+
callback(False)
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
self._was_tcp = prefer_tcp or config.protocol == Protocol.WIREGUARDTCP
|
|
351
|
+
self._should_failover = config.should_failover
|
|
352
|
+
|
|
353
|
+
def on_fail():
|
|
354
|
+
if self.common.in_state(State.CONNECTED):
|
|
355
|
+
self.common.set_state(State.DISCONNECTING)
|
|
356
|
+
self.common.set_state(State.DISCONNECTED)
|
|
357
|
+
if callback:
|
|
358
|
+
callback(False)
|
|
359
|
+
|
|
360
|
+
def on_success():
|
|
361
|
+
# failed to disconnected
|
|
362
|
+
if self.common.in_state(State.CONNECTING):
|
|
363
|
+
self.common.set_state(State.CONNECTED)
|
|
364
|
+
if callback:
|
|
365
|
+
callback(True)
|
|
366
|
+
|
|
367
|
+
def final_connected(dropped: bool, error: str = ""):
|
|
368
|
+
# failover reports not dropped, return to connected if we are still in connecting
|
|
369
|
+
if not dropped:
|
|
370
|
+
if self.common.in_state(State.CONNECTING):
|
|
371
|
+
on_success()
|
|
372
|
+
else:
|
|
373
|
+
on_fail()
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
# Connection is dropped!
|
|
377
|
+
def on_reconnected(success: bool):
|
|
378
|
+
if not success:
|
|
379
|
+
handle_exception(self.common, Exception("failed to reconnect with TCP"))
|
|
380
|
+
if success:
|
|
381
|
+
set_failovered(self.common)
|
|
382
|
+
# When an error happens we always set success now as the previous
|
|
383
|
+
# protocol might be connected to
|
|
384
|
+
# TODO: differentiate between disconnect and new connect errors
|
|
385
|
+
on_success()
|
|
386
|
+
|
|
387
|
+
# reconnect with TCP
|
|
388
|
+
self.reconnect_tcp(on_reconnected)
|
|
389
|
+
|
|
390
|
+
def on_connected(success: bool):
|
|
391
|
+
if success:
|
|
392
|
+
# failover should not continue
|
|
393
|
+
if not self.should_failover():
|
|
394
|
+
on_success()
|
|
395
|
+
else:
|
|
396
|
+
set_online_detecting(self.common)
|
|
397
|
+
# failover should start
|
|
398
|
+
self.start_failover(final_connected)
|
|
399
|
+
else:
|
|
400
|
+
on_fail()
|
|
401
|
+
|
|
402
|
+
def on_connect(success: bool):
|
|
403
|
+
if success:
|
|
404
|
+
self.nm_manager.activate_connection(on_connected)
|
|
405
|
+
else:
|
|
406
|
+
on_fail()
|
|
407
|
+
|
|
408
|
+
@run_in_glib_thread
|
|
409
|
+
def connect(config):
|
|
410
|
+
if not self.common.in_state(State.CONNECTING):
|
|
411
|
+
self.common.set_state(State.CONNECTING)
|
|
412
|
+
connection = Connection.parse(config)
|
|
413
|
+
connection.connect(
|
|
414
|
+
self.nm_manager,
|
|
415
|
+
config.default_gateway,
|
|
416
|
+
self.config.allow_wg_lan,
|
|
417
|
+
config.dns_search_domains,
|
|
418
|
+
config.proxy,
|
|
419
|
+
self._peer_ips_proxy,
|
|
420
|
+
on_connect,
|
|
421
|
+
)
|
|
422
|
+
self._peer_ips_proxy = None
|
|
423
|
+
|
|
424
|
+
if config.proxy:
|
|
425
|
+
self.start_proxy(config.proxy, lambda: connect(config))
|
|
426
|
+
else:
|
|
427
|
+
connect(config)
|
|
428
|
+
|
|
429
|
+
def reconnect(self, callback: Optional[Callable] = None, prefer_tcp: bool = False):
|
|
430
|
+
def on_disconnected(success: bool):
|
|
431
|
+
if success:
|
|
432
|
+
self.activate_connection(callback, prefer_tcp=prefer_tcp)
|
|
433
|
+
|
|
434
|
+
# Reconnect
|
|
435
|
+
self.deactivate_connection(on_disconnected)
|
|
436
|
+
|
|
437
|
+
# https://github.com/eduvpn/documentation/blob/v3/API.md#session-expiry
|
|
438
|
+
def renew_session(self, callback: Optional[Callable] = None):
|
|
439
|
+
was_connected = self.common.in_state(State.CONNECTED)
|
|
440
|
+
|
|
441
|
+
@run_in_background_thread("reconnect")
|
|
442
|
+
def reconnect(success: bool = True):
|
|
443
|
+
if not success:
|
|
444
|
+
if callback:
|
|
445
|
+
callback(False)
|
|
446
|
+
return
|
|
447
|
+
# Delete the OAuth access and refresh token
|
|
448
|
+
# Start the OAuth authorization flow
|
|
449
|
+
self.common.renew_session()
|
|
450
|
+
# Automatically reconnect to the server
|
|
451
|
+
self.activate_connection(callback)
|
|
452
|
+
|
|
453
|
+
if was_connected:
|
|
454
|
+
# Call /disconnect and reconnect with callback
|
|
455
|
+
self.deactivate_connection(reconnect)
|
|
456
|
+
else:
|
|
457
|
+
reconnect()
|
|
458
|
+
|
|
459
|
+
def disconnect(self, callback: Optional[Callable] = None) -> None:
|
|
460
|
+
self.nm_manager.deactivate_connection(callback)
|
|
461
|
+
|
|
462
|
+
def set_profile(self, profile: str, connect=False):
|
|
463
|
+
was_connected = self.common.in_state(State.CONNECTED)
|
|
464
|
+
|
|
465
|
+
def do_profile(success: bool = True):
|
|
466
|
+
if not success:
|
|
467
|
+
return
|
|
468
|
+
# Set the profile ID
|
|
469
|
+
self.common.set_profile(profile)
|
|
470
|
+
|
|
471
|
+
# Connect if we should and if we were previously connected
|
|
472
|
+
if connect and was_connected:
|
|
473
|
+
self.activate_connection()
|
|
474
|
+
|
|
475
|
+
# Deactivate connection if we are connected
|
|
476
|
+
# and the connection should be modified
|
|
477
|
+
# the do_profile will be called in the callback
|
|
478
|
+
if was_connected and connect:
|
|
479
|
+
self.deactivate_connection(do_profile)
|
|
480
|
+
else:
|
|
481
|
+
do_profile()
|
|
482
|
+
|
|
483
|
+
def activate_connection(self, callback: Optional[Callable] = None, prefer_tcp: bool = False):
|
|
484
|
+
if (
|
|
485
|
+
not self.common.in_state(State.GOT_CONFIG)
|
|
486
|
+
and not self.common.in_state(State.DISCONNECTED)
|
|
487
|
+
and not self.common.in_state(State.MAIN)
|
|
488
|
+
):
|
|
489
|
+
if callback:
|
|
490
|
+
logger.error("invalid state to activate connection")
|
|
491
|
+
callback(False)
|
|
492
|
+
return
|
|
493
|
+
if not self.current_server:
|
|
494
|
+
if callback:
|
|
495
|
+
logger.error("failed to get current server")
|
|
496
|
+
callback(False)
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
def on_connected(success: bool):
|
|
500
|
+
if callback:
|
|
501
|
+
callback(success)
|
|
502
|
+
|
|
503
|
+
self.connect(self.current_server, on_connected, prefer_tcp=prefer_tcp)
|
|
504
|
+
|
|
505
|
+
@run_in_background_thread("cleanup")
|
|
506
|
+
def cleanup(self, callback: Optional[Callable] = None):
|
|
507
|
+
# We retry this cleanup 2 times
|
|
508
|
+
retries = 2
|
|
509
|
+
|
|
510
|
+
# Try to cleanup with a number of retries
|
|
511
|
+
for i in range(retries):
|
|
512
|
+
logger.debug("Cleaning up tokens...")
|
|
513
|
+
try:
|
|
514
|
+
self.common.cleanup()
|
|
515
|
+
except Exception as e:
|
|
516
|
+
# We can try again
|
|
517
|
+
if i < retries - 1:
|
|
518
|
+
logger.debug(
|
|
519
|
+
f"Got an error: {str(e)} while cleaning up, try number: {i+1}. This could mean the connection was not fully disconnected yet. Trying again..."
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
# All retries are done
|
|
523
|
+
logger.debug(f"Got an error: {str(e)} while cleaning up, after full retries: {i+1}.")
|
|
524
|
+
else:
|
|
525
|
+
break
|
|
526
|
+
if self.common.in_state(State.DISCONNECTING):
|
|
527
|
+
self.common.set_state(State.DISCONNECTED)
|
|
528
|
+
if callback:
|
|
529
|
+
callback()
|
|
530
|
+
|
|
531
|
+
def deactivate_connection(self, callback: Optional[Callable] = None, cleanup=True) -> None:
|
|
532
|
+
curr = None
|
|
533
|
+
if self.common.in_state(State.CONNECTED):
|
|
534
|
+
curr = State.CONNECTED
|
|
535
|
+
|
|
536
|
+
if self.common.in_state(State.CONNECTING):
|
|
537
|
+
curr = State.CONNECTING
|
|
538
|
+
|
|
539
|
+
if not curr:
|
|
540
|
+
return
|
|
541
|
+
self.common.set_state(State.DISCONNECTING)
|
|
542
|
+
|
|
543
|
+
def on_disconnected(success: bool):
|
|
544
|
+
if success:
|
|
545
|
+
|
|
546
|
+
def on_cleaned():
|
|
547
|
+
self.common.cancel()
|
|
548
|
+
if callback:
|
|
549
|
+
callback(True)
|
|
550
|
+
|
|
551
|
+
if cleanup:
|
|
552
|
+
self.cleanup(on_cleaned)
|
|
553
|
+
else:
|
|
554
|
+
if self.common.in_state(State.DISCONNECTING):
|
|
555
|
+
self.common.set_state(State.DISCONNECTED)
|
|
556
|
+
on_cleaned()
|
|
557
|
+
else:
|
|
558
|
+
try:
|
|
559
|
+
self.common.set_state(curr)
|
|
560
|
+
except WrappedError as e:
|
|
561
|
+
logger.debug(f"set_state error in deactivate connection: {str(e)}")
|
|
562
|
+
if callback:
|
|
563
|
+
callback(False)
|
|
564
|
+
|
|
565
|
+
self.disconnect(on_disconnected)
|
|
566
|
+
|
|
567
|
+
def search_predefined(self, query: str) -> Iterator[Any]:
|
|
568
|
+
return self.server_db.search_predefined(query)
|
|
569
|
+
|
|
570
|
+
def search_custom(self, query: str) -> Iterator[Any]:
|
|
571
|
+
return self.server_db.search_custom(query)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class Application:
|
|
575
|
+
def __init__(self, variant: ApplicationVariant, common: EduVPN) -> None:
|
|
576
|
+
self.variant = variant
|
|
577
|
+
self.nm_manager = nm.NMManager(variant)
|
|
578
|
+
self.common = common
|
|
579
|
+
directory = variant.config_prefix
|
|
580
|
+
self.config = Configuration.load(directory)
|
|
581
|
+
self.model = ApplicationModel(common, self.config, variant, self.nm_manager)
|
|
582
|
+
|
|
583
|
+
signal.signal(signal.SIGINT, lambda s, f: self.cleanup_sigint(s, f))
|
|
584
|
+
|
|
585
|
+
def cleanup_sigint(self, _signal, _frame):
|
|
586
|
+
self.model.cancel()
|
|
587
|
+
self.common.deregister()
|
|
588
|
+
|
|
589
|
+
def on_network_update_callback(self, state, initial=False):
|
|
590
|
+
try:
|
|
591
|
+
if state == nm.ConnectionState.CONNECTED:
|
|
592
|
+
if (
|
|
593
|
+
not self.common.in_state(State.DISCONNECTED)
|
|
594
|
+
and not self.common.in_state(State.GOT_CONFIG)
|
|
595
|
+
and not initial
|
|
596
|
+
):
|
|
597
|
+
return
|
|
598
|
+
if not initial:
|
|
599
|
+
self.common.set_state(State.CONNECTING)
|
|
600
|
+
# Already connected
|
|
601
|
+
self.common.set_state(State.CONNECTED)
|
|
602
|
+
elif state == nm.ConnectionState.CONNECTING:
|
|
603
|
+
self.common.set_state(State.CONNECTING)
|
|
604
|
+
elif state == nm.ConnectionState.DISCONNECTED:
|
|
605
|
+
if not self.common.in_state(State.CONNECTED) and not self.common.in_state(State.CONNECTING):
|
|
606
|
+
return
|
|
607
|
+
self.common.set_state(State.DISCONNECTING)
|
|
608
|
+
self.model.cancel()
|
|
609
|
+
self.model.cleanup()
|
|
610
|
+
self.model.disconnect()
|
|
611
|
+
except Exception as e:
|
|
612
|
+
logger.debug(f"error occurred: {str(e)}")
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
def initialize_network(self, needs_update=True) -> None:
|
|
616
|
+
"""
|
|
617
|
+
Determine the current network state.
|
|
618
|
+
"""
|
|
619
|
+
# Check if a previous network configuration exists.
|
|
620
|
+
uuid = self.nm_manager.existing_connection
|
|
621
|
+
if uuid:
|
|
622
|
+
self.on_network_update_callback(self.nm_manager.connection_state, needs_update)
|
|
623
|
+
|
|
624
|
+
@run_in_background_thread("on-network-update")
|
|
625
|
+
def update(state):
|
|
626
|
+
self.on_network_update_callback(state, False)
|
|
627
|
+
|
|
628
|
+
self.nm_manager.subscribe_to_status_changes(update)
|