Furious-GUI 0.2.4__tar.gz → 0.2.5.dev0__tar.gz

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.
Files changed (74) hide show
  1. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Action/Connect.py +392 -48
  2. Furious-GUI-0.2.5.dev0/Furious/Action/EditConfiguration.py +32 -0
  3. Furious-GUI-0.2.5.dev0/Furious/Action/Exit.py +32 -0
  4. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Action/Export.py +22 -1
  5. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Action/Import.py +17 -0
  6. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Action/Language.py +17 -0
  7. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Action/Routing.py +17 -0
  8. Furious-GUI-0.2.5.dev0/Furious/Action/Settings.py +165 -0
  9. Furious-GUI-0.2.5.dev0/Furious/Action/__init__.py +24 -0
  10. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Core/Configuration.py +17 -0
  11. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Core/Core.py +33 -14
  12. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Core/Intellisense.py +17 -0
  13. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Core/TorRelay.py +60 -19
  14. Furious-GUI-0.2.5.dev0/Furious/Core/Tun2socks.py +67 -0
  15. Furious-GUI-0.2.5.dev0/Furious/Core/__init__.py +17 -0
  16. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Gui/Action.py +17 -0
  17. Furious-GUI-0.2.5.dev0/Furious/Gui/Icon.py +25 -0
  18. Furious-GUI-0.2.5.dev0/Furious/Gui/__init__.py +17 -0
  19. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Utility/Constants.py +36 -0
  20. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Utility/Process.py +17 -0
  21. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Utility/Proxy.py +19 -4
  22. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Utility/Resources.py +331 -32
  23. Furious-GUI-0.2.5.dev0/Furious/Utility/RoutingTable.py +239 -0
  24. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Utility/Settings.py +19 -0
  25. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Utility/StartupOnBoot.py +17 -0
  26. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Utility/Theme.py +17 -0
  27. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Utility/Translator.py +85 -12
  28. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Utility/Utility.py +56 -2
  29. Furious-GUI-0.2.5.dev0/Furious/Utility/__init__.py +17 -0
  30. Furious-GUI-0.2.5.dev0/Furious/Version.py +18 -0
  31. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/Application.py +19 -2
  32. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/AssetViewer.py +27 -0
  33. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/ConnectingProgressBar.py +17 -0
  34. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/EditConfiguration.py +48 -17
  35. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/EditRouting.py +42 -3
  36. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/ExportQRCode.py +17 -0
  37. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/IndentSpinBox.py +17 -0
  38. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/LogViewer.py +17 -0
  39. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/SystemTrayIcon.py +18 -1
  40. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/TorRelaySettings.py +35 -2
  41. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Widget/Widget.py +29 -6
  42. Furious-GUI-0.2.5.dev0/Furious/Widget/__init__.py +17 -0
  43. Furious-GUI-0.2.5.dev0/Furious/__init__.py +18 -0
  44. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/__main__.py +17 -0
  45. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0/Furious_GUI.egg-info}/PKG-INFO +33 -51
  46. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious_GUI.egg-info/SOURCES.txt +2 -0
  47. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious_GUI.egg-info/requires.txt +1 -0
  48. {Furious-GUI-0.2.4/Furious_GUI.egg-info → Furious-GUI-0.2.5.dev0}/PKG-INFO +33 -51
  49. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/README.md +32 -50
  50. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/setup.py +18 -0
  51. Furious-GUI-0.2.4/Furious/Action/EditConfiguration.py +0 -15
  52. Furious-GUI-0.2.4/Furious/Action/Exit.py +0 -15
  53. Furious-GUI-0.2.4/Furious/Action/Settings.py +0 -80
  54. Furious-GUI-0.2.4/Furious/Action/__init__.py +0 -7
  55. Furious-GUI-0.2.4/Furious/Core/__init__.py +0 -0
  56. Furious-GUI-0.2.4/Furious/Gui/Icon.py +0 -8
  57. Furious-GUI-0.2.4/Furious/Gui/__init__.py +0 -0
  58. Furious-GUI-0.2.4/Furious/Utility/__init__.py +0 -0
  59. Furious-GUI-0.2.4/Furious/Version.py +0 -1
  60. Furious-GUI-0.2.4/Furious/Widget/__init__.py +0 -0
  61. Furious-GUI-0.2.4/Furious/__init__.py +0 -1
  62. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Data/font/CascadiaMono +0 -0
  63. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Data/font/LICENSE +0 -0
  64. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Data/hysteria/bypass-Iran.acl +0 -0
  65. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Data/hysteria/bypass-mainland-China.acl +0 -0
  66. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Data/hysteria/country.mmdb +0 -0
  67. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Data/xray/geoip.dat +0 -0
  68. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Data/xray/geosite.dat +0 -0
  69. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious/Data/xray/iran.dat +0 -0
  70. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious_GUI.egg-info/dependency_links.txt +0 -0
  71. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious_GUI.egg-info/entry_points.txt +0 -0
  72. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/Furious_GUI.egg-info/top_level.txt +0 -0
  73. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/LICENSE +0 -0
  74. {Furious-GUI-0.2.4 → Furious-GUI-0.2.5.dev0}/setup.cfg +0 -0
@@ -1,5 +1,23 @@
1
+ # Copyright (C) 2023 Loren Eteval <loren.eteval@proton.me>
2
+ #
3
+ # This file is part of Furious.
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
1
18
  from Furious.Core.Core import XrayCore, Hysteria
2
19
  from Furious.Core.TorRelay import TorRelay
20
+ from Furious.Core.Tun2socks import Tun2socks
3
21
  from Furious.Core.Intellisense import Intellisense
4
22
  from Furious.Core.Configuration import Configuration
5
23
  from Furious.Action.Routing import BUILTIN_ROUTING_TABLE, BUILTIN_ROUTING
@@ -8,18 +26,29 @@ from Furious.Widget.ConnectingProgressBar import ConnectingProgressBar
8
26
  from Furious.Widget.Widget import MessageBox
9
27
  from Furious.Utility.Constants import (
10
28
  APP,
29
+ PLATFORM,
11
30
  APPLICATION_NAME,
12
31
  PROXY_SERVER_BYPASS,
32
+ APPLICATION_TUN_DEVICE_NAME,
33
+ APPLICATION_TUN_NETWORK_INTERFACE_NAME,
34
+ APPLICATION_TUN_IP_ADDRESS,
35
+ APPLICATION_TUN_GATEWAY_ADDRESS,
13
36
  DEFAULT_TOR_HTTPS_PORT,
37
+ DEFAULT_TOR_SOCKS_PORT,
38
+ DEFAULT_TOR_RELAY_ESTABLISH_TIMEOUT,
14
39
  )
15
40
  from Furious.Utility.Utility import (
16
41
  Switch,
17
42
  SupportConnectedCallback,
18
43
  bootstrapIcon,
19
44
  getAbsolutePath,
45
+ isValidIPAddress,
46
+ isAdministrator,
47
+ isVPNMode,
20
48
  )
21
49
  from Furious.Utility.Translator import gettext as _
22
50
  from Furious.Utility.Proxy import Proxy
51
+ from Furious.Utility.RoutingTable import RoutingTable
23
52
 
24
53
  from PySide6 import QtCore
25
54
  from PySide6.QtTest import QTest
@@ -37,7 +66,11 @@ import logging
37
66
  logger = logging.getLogger(__name__)
38
67
 
39
68
 
40
- class HttpProxyServerError(Exception):
69
+ class HttpsProxyServerError(Exception):
70
+ pass
71
+
72
+
73
+ class SocksProxyServerError(Exception):
41
74
  pass
42
75
 
43
76
 
@@ -55,9 +88,10 @@ class ConnectAction(Action):
55
88
  self.configurationIcingBox = MessageBox()
56
89
  self.configurationErrorBox = MessageBox()
57
90
  self.configurationTampered = MessageBox()
58
- self.httpProxyConfErrorBox = MessageBox()
91
+ self.configurationProxyErr = MessageBox()
59
92
 
60
- self.proxyServer = ''
93
+ self.httpsProxyServer = ''
94
+ self.socksProxyServer = ''
61
95
 
62
96
  self.networkAccessManager = QNetworkAccessManager(parent=self)
63
97
  self.networkReply = None
@@ -89,6 +123,7 @@ class ConnectAction(Action):
89
123
 
90
124
  self.XrayCore = XrayCore()
91
125
  self.Hysteria = Hysteria()
126
+ self.Tun2socks = Tun2socks()
92
127
  self.TorRelay = TorRelay()
93
128
 
94
129
  def XrayCoreExitCallback(self, exitcode):
@@ -97,6 +132,10 @@ class ConnectAction(Action):
97
132
  assert self.coreRunning
98
133
  assert self.coreName == XrayCore.name()
99
134
 
135
+ if exitcode == XrayCore.ExitCode.SystemShuttingDown:
136
+ # System shutting down. Do nothing
137
+ return
138
+
100
139
  if exitcode == XrayCore.ExitCode.ConfigurationError:
101
140
  if not self.isConnecting():
102
141
  # Protect connecting action. Mandatory
@@ -142,6 +181,10 @@ class ConnectAction(Action):
142
181
  assert self.coreRunning
143
182
  assert self.coreName == Hysteria.name()
144
183
 
184
+ if exitcode == Hysteria.ExitCode.SystemShuttingDown:
185
+ # System shutting down. Do nothing
186
+ return
187
+
145
188
  if exitcode == Hysteria.ExitCode.ConfigurationError:
146
189
  if not self.isConnecting():
147
190
  # Protect connecting action. Mandatory
@@ -181,7 +224,27 @@ class ConnectAction(Action):
181
224
  f'{Hysteria.name()}: {_("Core terminated unexpectedly")}'
182
225
  )
183
226
 
227
+ def Tun2socksExitCallback(self, exitcode):
228
+ if exitcode == Tun2socks.ExitCode.SystemShuttingDown:
229
+ # System shutting down. Do nothing
230
+ return
231
+
232
+ if not self.isConnecting():
233
+ # Protect connecting action. Mandatory
234
+ self.disconnectAction(
235
+ f'{Tun2socks.name()}: {_("Core terminated unexpectedly")}'
236
+ )
237
+ else:
238
+ self.coreRunning = False
239
+ self.disconnectReason = (
240
+ f'{Tun2socks.name()}: {_("Core terminated unexpectedly")}'
241
+ )
242
+
184
243
  def stopCore(self):
244
+ self.stopTun2socks()
245
+
246
+ self.TorRelay.stop()
247
+
185
248
  self.XrayCore.registerExitCallback(None)
186
249
  self.Hysteria.registerExitCallback(None)
187
250
 
@@ -189,8 +252,6 @@ class ConnectAction(Action):
189
252
  self.XrayCore.stop()
190
253
  self.Hysteria.stop()
191
254
 
192
- self.TorRelay.stop()
193
-
194
255
  self.coreRunning = False
195
256
 
196
257
  def showConnectingProgressBar(self):
@@ -225,6 +286,13 @@ class ConnectAction(Action):
225
286
 
226
287
  APP().tray.RoutingAction.setDisabled(value)
227
288
 
289
+ if (PLATFORM == 'Windows' or PLATFORM == 'Darwin') and isAdministrator():
290
+ VPNModeAction = APP().tray.SettingsAction.getVPNModeAction()
291
+
292
+ assert VPNModeAction is not None
293
+
294
+ VPNModeAction.setDisabled(value)
295
+
228
296
  def setConnectingStatus(self, showProgressBar=True):
229
297
  if showProgressBar:
230
298
  self.showConnectingProgressBar()
@@ -267,7 +335,8 @@ class ConnectAction(Action):
267
335
  APP().Connect = Switch.OFF
268
336
  APP().tray.setPlainIcon()
269
337
 
270
- self.proxyServer = ''
338
+ self.httpsProxyServer = ''
339
+ self.socksProxyServer = ''
271
340
 
272
341
  self.coreName = ''
273
342
  self.coreText = ''
@@ -325,21 +394,29 @@ class ConnectAction(Action):
325
394
  # Show the MessageBox and wait for user to close it
326
395
  self.configurationIcingBox.exec()
327
396
 
328
- def errorHttpProxyConf(self):
329
- self.httpProxyConfErrorBox.setIcon(MessageBox.Icon.Critical)
330
- self.httpProxyConfErrorBox.setWindowTitle(_('Unable to connect'))
331
- self.httpProxyConfErrorBox.setText(
397
+ def errorProxyConf(self, proxyType):
398
+ assert proxyType == 'http' or proxyType == 'socks'
399
+
400
+ self.configurationProxyErr.setIcon(MessageBox.Icon.Critical)
401
+ self.configurationProxyErr.setWindowTitle(_('Unable to connect'))
402
+ self.configurationProxyErr.setText(
332
403
  _(
333
- f'{APPLICATION_NAME} cannot find any valid http proxy '
404
+ f'{APPLICATION_NAME} cannot find any valid {proxyType} proxy '
334
405
  f'endpoint in your server configuration.'
335
406
  )
336
407
  )
337
- self.httpProxyConfErrorBox.setInformativeText(
408
+ self.configurationProxyErr.setInformativeText(
338
409
  _('Please complete your server configuration.')
339
410
  )
340
411
 
341
412
  # Show the MessageBox and wait for user to close it
342
- self.httpProxyConfErrorBox.exec()
413
+ self.configurationProxyErr.exec()
414
+
415
+ def errorHttpsProxyConf(self):
416
+ self.errorProxyConf('http')
417
+
418
+ def errorSocksProxyConf(self):
419
+ self.errorProxyConf('socks')
343
420
 
344
421
  @property
345
422
  def torRelayStorageObj(self):
@@ -347,8 +424,8 @@ class ConnectAction(Action):
347
424
  return APP().torRelaySettingsWidget.StorageObj
348
425
 
349
426
  def configureCore(self):
350
- def validateProxyServer(server):
351
- # Validate proxy server
427
+ def validateHttpsProxyServer(server):
428
+ # Validate https proxy server
352
429
  try:
353
430
  host, port = server.split(':')
354
431
 
@@ -358,11 +435,30 @@ class ConnectAction(Action):
358
435
  # Any non-exit exceptions
359
436
 
360
437
  self.reset()
361
- self.errorHttpProxyConf()
438
+ self.errorHttpsProxyConf()
362
439
 
363
440
  return False
364
441
  else:
365
- self.proxyServer = server
442
+ self.httpsProxyServer = server
443
+
444
+ return True
445
+
446
+ def validateSocksProxyServer(server):
447
+ # Validate socks proxy server
448
+ try:
449
+ host, port = server.split(':')
450
+
451
+ if int(port) < 0 or int(port) > 65535:
452
+ raise ValueError
453
+ except Exception:
454
+ # Any non-exit exceptions
455
+
456
+ self.reset()
457
+ self.errorSocksProxyConf()
458
+
459
+ return False
460
+ else:
461
+ self.socksProxyServer = server
366
462
 
367
463
  return True
368
464
 
@@ -374,34 +470,69 @@ class ConnectAction(Action):
374
470
  self.Hysteria.registerExitCallback(
375
471
  lambda exitcode: self.HysteriaExitCallback(exitcode)
376
472
  )
473
+ self.Tun2socks.registerExitCallback(
474
+ lambda exitcode: self.Tun2socksExitCallback(exitcode)
475
+ )
377
476
 
378
- proxyServer = None
477
+ httpsProxyServer = None
478
+ socksProxyServer = None
379
479
 
380
480
  if Intellisense.getCoreType(self.coreJSON) == XrayCore.name():
381
481
  # Assuming is XrayCore configuration
382
- proxyHost = None
383
- proxyPort = None
482
+ httpsProxyHost = None
483
+ httpsProxyPort = None
484
+ socksProxyHost = None
485
+ socksProxyPort = None
486
+
487
+ if isVPNMode():
488
+ # Check socks inbound is valid
489
+ try:
490
+ for inbound in self.coreJSON['inbounds']:
491
+ if inbound['protocol'] == 'socks':
492
+ socksProxyHost = inbound['listen']
493
+ socksProxyPort = inbound['port']
494
+
495
+ # Note: If there are multiple socks inbounds
496
+ # satisfied, the first one will be chosen.
497
+ break
498
+
499
+ if socksProxyHost is None or socksProxyPort is None:
500
+ # No HTTP proxy endpoint configured
501
+ raise SocksProxyServerError
502
+
503
+ socksProxyServer = f'{socksProxyHost}:{socksProxyPort}'
504
+ except (KeyError, SocksProxyServerError):
505
+ self.reset()
506
+ self.errorSocksProxyConf()
507
+
508
+ # Return to caller
509
+ return XrayCore.name()
510
+ else:
511
+ # Validate socks proxy server
512
+ if not validateSocksProxyServer(socksProxyServer):
513
+ # Return to caller
514
+ return XrayCore.name()
384
515
 
385
516
  try:
386
517
  for inbound in self.coreJSON['inbounds']:
387
518
  if inbound['protocol'] == 'http':
388
- proxyHost = inbound['listen']
389
- proxyPort = inbound['port']
519
+ httpsProxyHost = inbound['listen']
520
+ httpsProxyPort = inbound['port']
390
521
 
391
522
  # Note: If there are multiple http inbounds
392
523
  # satisfied, the first one will be chosen.
393
524
  break
394
525
 
395
- if proxyHost is None or proxyPort is None:
526
+ if httpsProxyHost is None or httpsProxyPort is None:
396
527
  # No HTTP proxy endpoint configured
397
- raise HttpProxyServerError
528
+ raise HttpsProxyServerError
398
529
 
399
- proxyServer = f'{proxyHost}:{proxyPort}'
400
- except (KeyError, HttpProxyServerError):
530
+ httpsProxyServer = f'{httpsProxyHost}:{httpsProxyPort}'
531
+ except (KeyError, HttpsProxyServerError):
401
532
  self.reset()
402
- self.errorHttpProxyConf()
533
+ self.errorHttpsProxyConf()
403
534
  else:
404
- if validateProxyServer(proxyServer):
535
+ if validateHttpsProxyServer(httpsProxyServer):
405
536
  routing = APP().Routing
406
537
 
407
538
  logger.info(f'core {XrayCore.name()} configured')
@@ -463,7 +594,6 @@ class ConnectAction(Action):
463
594
  logger.error('find Tor CLI in path failed')
464
595
 
465
596
  self.coreRunning = False
466
-
467
597
  self.disconnectReason = (
468
598
  f'{XrayCore.name()}: {_("Cannot find Tor CLI in PATH")}'
469
599
  )
@@ -502,26 +632,57 @@ class ConnectAction(Action):
502
632
  self.coreJSON, ensure_ascii=False, escape_forward_slashes=False
503
633
  )
504
634
  # Start core
505
- self.XrayCore.start(self.coreText)
635
+ if self.coreJSON:
636
+ self.XrayCore.start(self.coreText)
637
+
638
+ if routing == 'Route My Traffic Through Tor':
639
+ # Must start Tor Relay first since it will redirect proxy for us
640
+ self.startTorRelay(XrayCore.name(), httpsProxyServer)
506
641
 
507
- if routing == 'Route My Traffic Through Tor':
508
- self.startTorRelay(XrayCore.name(), proxyServer)
642
+ if isVPNMode():
643
+ if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
644
+ # Currently VPN Mode is only supported on Windows and macOS
645
+ self.startTun2socks()
646
+ else:
647
+ # Fast fail
648
+ self.XrayCore.start(self.coreText, waitTime=1000)
509
649
 
510
650
  return XrayCore.name()
511
651
 
512
652
  if Intellisense.getCoreType(self.coreJSON) == Hysteria.name():
513
653
  # Assuming is Hysteria configuration
654
+
655
+ if isVPNMode():
656
+ # Check socks inbound is valid
657
+ try:
658
+ socksProxyServer = self.coreJSON['socks5']['listen']
659
+
660
+ if socksProxyServer is None:
661
+ # No SOCKS proxy endpoint configured
662
+ raise SocksProxyServerError
663
+ except (KeyError, SocksProxyServerError):
664
+ self.reset()
665
+ self.errorSocksProxyConf()
666
+
667
+ # Return to caller
668
+ return Hysteria.name()
669
+ else:
670
+ # Validate socks proxy server
671
+ if not validateSocksProxyServer(socksProxyServer):
672
+ # Return to caller
673
+ return Hysteria.name()
674
+
514
675
  try:
515
- proxyServer = self.coreJSON['http']['listen']
676
+ httpsProxyServer = self.coreJSON['http']['listen']
516
677
 
517
- if proxyServer is None:
678
+ if httpsProxyServer is None:
518
679
  # No HTTP proxy endpoint configured
519
- raise HttpProxyServerError
520
- except (KeyError, HttpProxyServerError):
680
+ raise HttpsProxyServerError
681
+ except (KeyError, HttpsProxyServerError):
521
682
  self.reset()
522
- self.errorHttpProxyConf()
683
+ self.errorHttpsProxyConf()
523
684
  else:
524
- if validateProxyServer(proxyServer):
685
+ if validateHttpsProxyServer(httpsProxyServer):
525
686
  self.coreRunning = True
526
687
 
527
688
  routing = APP().Routing
@@ -556,12 +717,11 @@ class ConnectAction(Action):
556
717
 
557
718
  self.Hysteria.start(self.coreText, '', '')
558
719
 
559
- self.startTorRelay(Hysteria.name(), proxyServer)
720
+ self.startTorRelay(Hysteria.name(), httpsProxyServer)
560
721
  else:
561
722
  logger.error('find Tor CLI in path failed')
562
723
 
563
724
  self.coreRunning = False
564
-
565
725
  self.disconnectReason = (
566
726
  f'{Hysteria.name()}: {_("Cannot find Tor CLI in PATH")}'
567
727
  )
@@ -601,26 +761,206 @@ class ConnectAction(Action):
601
761
  )
602
762
 
603
763
  # Fast fail
604
- self.Hysteria.start('', '', '')
764
+ self.coreJSON = {}
765
+ self.coreText = ''
766
+
767
+ self.Hysteria.start(self.coreText, '', '', waitTime=1000)
768
+
769
+ if self.coreJSON:
770
+ if isVPNMode():
771
+ if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
772
+ # Currently VPN Mode is only supported on Windows and macOS
773
+ self.startTun2socks()
605
774
 
606
775
  return Hysteria.name()
607
776
 
608
777
  # No matching core
609
778
  return ''
610
779
 
780
+ def startTun2socks(self, successCallback=None):
781
+ if not self.coreRunning:
782
+ # Core has exited. Do nothing
783
+ return
784
+
785
+ defaultGateway = list(
786
+ # Filter TUN Gateway
787
+ filter(
788
+ lambda x: x != APPLICATION_TUN_GATEWAY_ADDRESS,
789
+ RoutingTable.getDefaultGatewayAddress(),
790
+ )
791
+ )
792
+
793
+ if len(defaultGateway) != 1:
794
+ logger.error(f'found multiple gateway addresses: {defaultGateway}')
795
+
796
+ self.coreRunning = False
797
+ self.disconnectReason = (
798
+ _('Unable to connect') + ': ' + _('Multiple gateway addresses found')
799
+ )
800
+
801
+ return
802
+
803
+ coreAddr = Intellisense.getCoreAddr(self.coreJSON)
804
+
805
+ if not coreAddr:
806
+ logger.error(f'invalid server address: {coreAddr}')
807
+
808
+ self.coreRunning = False
809
+ self.disconnectReason = _('Server address invalid') + f': {coreAddr}'
810
+
811
+ return
812
+
813
+ def start():
814
+ assert RoutingTable.Relations
815
+
816
+ if PLATFORM == 'Darwin':
817
+ for source in [
818
+ *list(f'{2 ** (8 - x)}.0.0.0/{x}' for x in range(8, 0, -1)),
819
+ '198.18.0.0/15',
820
+ ]:
821
+ RoutingTable.Relations.append(
822
+ [source, APPLICATION_TUN_GATEWAY_ADDRESS]
823
+ )
824
+
825
+ self.Tun2socks.start(
826
+ APPLICATION_TUN_DEVICE_NAME,
827
+ APPLICATION_TUN_NETWORK_INTERFACE_NAME,
828
+ 'silent',
829
+ f'socks5://{self.socksProxyServer}',
830
+ '',
831
+ )
832
+
833
+ if PLATFORM == 'Windows':
834
+ RoutingTable.addRelations()
835
+ RoutingTable.setDeviceGatewayAddress(
836
+ APPLICATION_TUN_DEVICE_NAME,
837
+ APPLICATION_TUN_IP_ADDRESS,
838
+ APPLICATION_TUN_GATEWAY_ADDRESS,
839
+ )
840
+
841
+ if PLATFORM == 'Darwin':
842
+ RoutingTable.setDeviceGatewayAddress(
843
+ APPLICATION_TUN_DEVICE_NAME,
844
+ APPLICATION_TUN_IP_ADDRESS,
845
+ APPLICATION_TUN_GATEWAY_ADDRESS,
846
+ )
847
+ RoutingTable.addRelations()
848
+
849
+ if callable(successCallback):
850
+ successCallback()
851
+
852
+ if not isValidIPAddress(coreAddr):
853
+ logger.info(f'dns resolve uses proxy server {self.httpsProxyServer}')
854
+
855
+ # Checked. split should not throw exceptions
856
+ proxyHost, proxyPort = self.httpsProxyServer.split(':')
857
+
858
+ # Checked. int(proxyPort) should not throw exceptions
859
+ self.networkAccessManager.setProxy(
860
+ QNetworkProxy(
861
+ QNetworkProxy.ProxyType.HttpProxy, proxyHost, int(proxyPort)
862
+ )
863
+ )
864
+
865
+ request = QNetworkRequest(
866
+ QtCore.QUrl(f'https://cloudflare-dns.com/dns-query?name={coreAddr}')
867
+ )
868
+ request.setRawHeader('accept'.encode(), 'application/dns-json'.encode())
869
+
870
+ self.networkReply = self.networkAccessManager.get(request)
871
+
872
+ @QtCore.Slot()
873
+ def finishedCallback():
874
+ if self.networkReply is None:
875
+ # Interrupted externally. Do nothing
876
+ logger.info(f'dns resolve for {coreAddr} interrupted externally')
877
+
878
+ return
879
+
880
+ assert isinstance(self.networkReply, QNetworkReply)
881
+
882
+ if self.networkReply.error() != QNetworkReply.NetworkError.NoError:
883
+ logger.error(
884
+ f'DNS resolution for {coreAddr} failed. {self.networkReply.errorString()}'
885
+ )
886
+
887
+ self.coreRunning = False
888
+ self.disconnectReason = _('DNS resolution failed') + f': {coreAddr}'
889
+ # Reset reply
890
+ self.networkReply = None
891
+ else:
892
+ logger.info(f'dns resolve for {coreAddr} success')
893
+
894
+ replyObject = ujson.loads(self.networkReply.readAll().data())
895
+
896
+ # Reset reply
897
+ self.networkReply = None
898
+
899
+ for record in replyObject['Answer']:
900
+ address = record['data']
901
+
902
+ logger.info(f'{coreAddr} resolved to {address}')
903
+
904
+ RoutingTable.Relations.append([address, defaultGateway[0]])
905
+
906
+ start()
907
+
908
+ self.networkReply.finished.connect(finishedCallback)
909
+ else:
910
+ # Reset reply
911
+ self.networkReply = None
912
+
913
+ RoutingTable.Relations.append([coreAddr, defaultGateway[0]])
914
+
915
+ start()
916
+
917
+ self.waitForDNSResolutionIfNecessary()
918
+
919
+ def stopTun2socks(self):
920
+ self.Tun2socks.registerExitCallback(None)
921
+ self.Tun2socks.stop()
922
+
923
+ def waitForDNSResolutionIfNecessary(self, startCounter=0, timeout=10000, step=1):
924
+ # Wait for potentially DNS resolve in VPN Mode
925
+ if self.coreRunning and self.networkReply is not None:
926
+ logger.info('dns resolve in progress. Wait')
927
+ else:
928
+ return
929
+
930
+ while (
931
+ self.coreRunning
932
+ and startCounter < timeout
933
+ and self.networkReply is not None
934
+ ):
935
+ QTest.qWait(step)
936
+
937
+ startCounter += step
938
+
939
+ if self.networkReply is not None:
940
+ if self.coreRunning:
941
+ logger.error('DNS resolution timeout')
942
+
943
+ self.coreRunning = False
944
+ self.disconnectReason = _('DNS resolution timeout')
945
+
946
+ # Reset reply
947
+ self.networkReply = None
948
+
611
949
  def startTorRelay(self, core, proxyServer, startCounter=0, step=1):
612
950
  # Redirect Proxy
613
- self.proxyServer = f'127.0.0.1:{self.torRelayStorageObj.get("httpsTunnelPort", DEFAULT_TOR_HTTPS_PORT)}'
951
+ self.httpsProxyServer = f'127.0.0.1:{self.torRelayStorageObj.get("httpsTunnelPort", DEFAULT_TOR_HTTPS_PORT)}'
952
+ self.socksProxyServer = f'127.0.0.1:{self.torRelayStorageObj.get("socksTunnelPort", DEFAULT_TOR_SOCKS_PORT)}'
614
953
 
615
954
  try:
616
955
  timeout = 1000 * int(
617
- self.torRelayStorageObj.get('relayEstablishTimeout', 15)
956
+ self.torRelayStorageObj.get(
957
+ 'relayEstablishTimeout', DEFAULT_TOR_RELAY_ESTABLISH_TIMEOUT
958
+ )
618
959
  )
619
960
  except Exception:
620
961
  # Any non-exit exceptions
621
962
 
622
- # 15s
623
- timeout = 1000 * 15
963
+ timeout = 1000 * DEFAULT_TOR_RELAY_ESTABLISH_TIMEOUT
624
964
 
625
965
  logger.info(f'{TorRelay.name()} bootstrap timeout is {timeout // 1000}s')
626
966
 
@@ -665,9 +1005,10 @@ class ConnectAction(Action):
665
1005
  self.testTime += 1
666
1006
 
667
1007
  logger.info(f'start connection test. Try: {selected}')
1008
+ logger.info(f'connection test uses proxy server {self.httpsProxyServer}')
668
1009
 
669
1010
  # Checked. split should not throw exceptions
670
- proxyHost, proxyPort = self.proxyServer.split(':')
1011
+ proxyHost, proxyPort = self.httpsProxyServer.split(':')
671
1012
 
672
1013
  # Checked. int(proxyPort) should not throw exceptions
673
1014
  self.networkAccessManager.setProxy(
@@ -795,6 +1136,9 @@ class ConnectAction(Action):
795
1136
  # Connecting status
796
1137
  self.setConnectingStatus(showProgressBar)
797
1138
 
1139
+ # Reset reply. Mandatory
1140
+ self.networkReply = None
1141
+
798
1142
  # Configure connect
799
1143
  self.coreName = self.configureCore()
800
1144
 
@@ -806,7 +1150,7 @@ class ConnectAction(Action):
806
1150
  return
807
1151
 
808
1152
  if not self.coreRunning:
809
- # 1. No valid HTTP proxy endpoint. reset / disconnect has been called
1153
+ # 1. No valid HTTP/Socks proxy endpoint. reset / disconnect has been called
810
1154
 
811
1155
  if self.isConnecting():
812
1156
  # 2. Core has exited
@@ -816,7 +1160,7 @@ class ConnectAction(Action):
816
1160
  return
817
1161
 
818
1162
  try:
819
- Proxy.set(self.proxyServer, PROXY_SERVER_BYPASS)
1163
+ Proxy.set(self.httpsProxyServer, PROXY_SERVER_BYPASS)
820
1164
  except Exception:
821
1165
  # Any non-exit exceptions
822
1166
 
@@ -0,0 +1,32 @@
1
+ # Copyright (C) 2023 Loren Eteval <loren.eteval@proton.me>
2
+ #
3
+ # This file is part of Furious.
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ from Furious.Gui.Action import Action
19
+ from Furious.Utility.Constants import APP
20
+ from Furious.Utility.Utility import bootstrapIcon
21
+ from Furious.Utility.Translator import gettext as _
22
+
23
+
24
+ class EditConfigurationAction(Action):
25
+ def __init__(self):
26
+ super().__init__(
27
+ _('Edit Configuration...'),
28
+ icon=bootstrapIcon('pencil-square.svg'),
29
+ )
30
+
31
+ def triggeredCallback(self, checked):
32
+ APP().ServerWidget.show()