Add Tor Routing support

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2023-08-30 17:03:32 +08:00
parent 8695377245
commit ff58f6c0ff
15 changed files with 743 additions and 37 deletions
+3
View File
@@ -160,3 +160,6 @@ cython_debug/
.idea/
.DS_Store
# Tor data
Furious/Data/tordata
+121 -6
View File
@@ -1,4 +1,5 @@
from Furious.Core.Core import XrayCore, Hysteria
from Furious.Core.TorRelay import TorRelay
from Furious.Core.Intellisense import Intellisense
from Furious.Core.Configuration import Configuration
from Furious.Action.Routing import BUILTIN_ROUTING_TABLE, BUILTIN_ROUTING
@@ -16,6 +17,7 @@ from Furious.Utility.Translator import gettext as _
from Furious.Utility.Proxy import Proxy
from PySide6 import QtCore
from PySide6.QtTest import QTest
from PySide6.QtNetwork import (
QNetworkAccessManager,
QNetworkReply,
@@ -82,6 +84,7 @@ class ConnectAction(Action):
self.XrayCore = XrayCore()
self.Hysteria = Hysteria()
self.TorRelay = TorRelay()
def XrayCoreExitCallback(self, exitcode):
if self.coreName:
@@ -181,6 +184,8 @@ class ConnectAction(Action):
self.XrayCore.stop()
self.Hysteria.stop()
self.TorRelay.stop()
self.coreRunning = False
def showConnectingProgressBar(self):
@@ -331,6 +336,11 @@ class ConnectAction(Action):
# Show the MessageBox and wait for user to close it
self.httpProxyConfErrorBox.exec()
@property
def torRelayStorageObj(self):
# Handy reference
return APP().torRelaySettingsWidget.StorageObj
def configureCore(self):
def validateProxyServer(server):
# Validate proxy server
@@ -387,8 +397,6 @@ class ConnectAction(Action):
self.errorHttpProxyConf()
else:
if validateProxyServer(proxyServer):
self.coreRunning = True
routing = APP().Routing
logger.info(f'core {XrayCore.name()} configured')
@@ -419,9 +427,13 @@ class ConnectAction(Action):
fixLoggingRelativePath('access')
fixLoggingRelativePath('error')
# Filter Custom
# Filter Route My Traffic Through Tor, Custom
if routing in list(
filter(lambda x: x != 'Custom', BUILTIN_ROUTING)
filter(
lambda x: x != 'Route My Traffic Through Tor'
and x != 'Custom',
BUILTIN_ROUTING,
)
):
routingObject = BUILTIN_ROUTING_TABLE[routing][XrayCore.name()]
@@ -429,6 +441,29 @@ class ConnectAction(Action):
logger.info(f'RoutingObject: {routingObject}')
self.coreJSON['routing'] = routingObject
elif routing == 'Route My Traffic Through Tor':
logger.info(f'routing is {routing}')
if TorRelay.checkIfExists():
logger.info(
f'find Tor CLI in path success. Version: {TorRelay.version()}'
)
routingObject = {}
logger.info(f'RoutingObject: {routingObject}')
self.coreJSON['routing'] = routingObject
else:
logger.error('find Tor CLI in path failed')
self.coreRunning = False
self.disconnectReason = (
f'{XrayCore.name()}: {_("Cannot find Tor CLI in PATH")}'
)
return XrayCore.name()
elif routing == 'Custom':
logger.info(f'routing is {routing}')
logger.info(f'RoutingObject: {self.XrayRouting}')
@@ -455,6 +490,8 @@ class ConnectAction(Action):
# Fast fail
self.coreJSON = {}
self.coreRunning = True
# Refresh configuration modified before. User cannot feel
self.coreText = ujson.dumps(
self.coreJSON, ensure_ascii=False, escape_forward_slashes=False
@@ -462,6 +499,9 @@ class ConnectAction(Action):
# Start core
self.XrayCore.start(self.coreText)
if routing == 'Route My Traffic Through Tor':
self.startTorRelay(XrayCore.name(), proxyServer)
return XrayCore.name()
if Intellisense.getCoreType(self.coreJSON) == Hysteria.name():
@@ -483,10 +523,13 @@ class ConnectAction(Action):
logger.info(f'core {Hysteria.name()} configured')
# Filter Global, Custom
# Filter Route My Traffic Through Tor, Global, Custom
if routing in list(
filter(
lambda x: x != 'Global' and x != 'Custom', BUILTIN_ROUTING
lambda x: x != 'Route My Traffic Through Tor'
and x != 'Global'
and x != 'Custom',
BUILTIN_ROUTING,
)
):
logger.info(f'routing is {routing}')
@@ -498,6 +541,27 @@ class ConnectAction(Action):
Hysteria.rule(routingObject.get('acl')),
Hysteria.mmdb(routingObject.get('mmdb')),
)
elif routing == 'Route My Traffic Through Tor':
logger.info(f'routing is {routing}')
if TorRelay.checkIfExists():
logger.info(
f'find Tor CLI in path success. Version: {TorRelay.version()}'
)
self.Hysteria.start(self.coreText, '', '')
self.startTorRelay(Hysteria.name(), proxyServer)
else:
logger.error('find Tor CLI in path failed')
self.coreRunning = False
self.disconnectReason = (
f'{Hysteria.name()}: {_("Cannot find Tor CLI in PATH")}'
)
return Hysteria.name()
elif routing == 'Global':
logger.info(f'routing is {routing}')
@@ -539,6 +603,57 @@ class ConnectAction(Action):
# No matching core
return ''
def startTorRelay(self, core, proxyServer, startCounter=0, step=1):
# Redirect Proxy
self.proxyServer = (
f'127.0.0.1:{self.torRelayStorageObj.get("httpsTunnelPort", 9049)}'
)
try:
timeout = 1000 * int(
self.torRelayStorageObj.get('relayEstablishTimeout', 15)
)
except Exception:
# Any non-exit exceptions
# 15s
timeout = 1000 * 15
logger.info(f'{TorRelay.name()} bootstrap timeout is {timeout // 1000}s')
# Start Tor Relay
if self.torRelayStorageObj.get('useProxy', True):
# Use proxy
self.TorRelay.start(proxyServer=proxyServer)
else:
# Do not use proxy
self.TorRelay.start()
while (
self.coreRunning
and startCounter < timeout
and self.TorRelay.bootstrapPercentage != 100
):
QTest.qWait(step)
startCounter += step
if not self.coreRunning:
# Interrupted externally. Return
return
if self.TorRelay.bootstrapPercentage != 100:
# bootstrap timeout
self.stopCore()
# Assign disconnect reason
self.disconnectReason = (
f'{core}: {_(f"{TorRelay.name()} establish timeout")}'
)
logger.error(f'{TorRelay.name()} establish failed')
else:
logger.info(f'{TorRelay.name()} establish success')
def startConnectionTest(
self, showRoutingChangedMessage=False, currentRouting='', isBuiltinRouting=False
):
+4
View File
@@ -95,6 +95,10 @@ BUILTIN_ROUTING_TABLE = {
'mmdb': (DATA_DIR / 'hysteria' / 'country.mmdb').as_posix(),
},
},
'Route My Traffic Through Tor': {
XrayCore.name(): {},
Hysteria.name(): {},
},
'Global': {
XrayCore.name(): {
'domainStrategy': 'IPIfNonMatch',
+21 -1
View File
@@ -1,4 +1,4 @@
from Furious.Gui.Action import Action
from Furious.Gui.Action import Action, Seperator
from Furious.Widget.Widget import Menu
from Furious.Utility.Constants import APP
from Furious.Utility.Utility import bootstrapIcon, Switch
@@ -34,6 +34,22 @@ class SettingsChildAction(Action):
APP().ServerWidget.showTabAndSpacesIfNecessary()
class RoutingChildAction(Action):
def __init__(self, **kwargs):
super().__init__(_('Routing Settings...'), **kwargs)
def triggeredCallback(self, checked):
APP().RoutesWidget.show()
class TorRelaySettingsChildAction(Action):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def triggeredCallback(self, checked):
APP().torRelaySettingsWidget.open()
class SettingsAction(Action):
def __init__(self):
super().__init__(
@@ -55,6 +71,10 @@ class SettingsAction(Action):
checkable=True,
checked=APP().ShowTabAndSpacesInEditor == Switch.ON_,
),
Seperator(),
RoutingChildAction(),
Seperator(),
TorRelaySettingsChildAction(_('Tor Relay Settings...')),
),
useActionGroup=False,
)
+1 -1
View File
@@ -63,7 +63,7 @@ class Core:
if self.checkAlive():
# Start core daemon
self._daemonTimer.start(100)
self._daemonTimer.start(1)
def stop(self):
if (
+150
View File
@@ -0,0 +1,150 @@
from Furious.Core.Core import Core
from Furious.Utility.Constants import (
APP,
DATA_DIR,
DEFAULT_TOR_SOCKS_PORT,
DEFAULT_TOR_HTTPS_PORT,
)
from Furious.Utility.Utility import AsyncSubprocessMessage
from PySide6 import QtCore
import re
import signal
import logging
import threading
import subprocess
logger = logging.getLogger(__name__)
class TorRelayStarter(AsyncSubprocessMessage):
BOOTSTRAP_STATUS = re.compile('Bootstrapped ([0-9]+)%')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.connectTimeoutCallback(self.processLine)
self.torRelay = None
self.bootstrapPercentage = 0
@property
def torRelayStorageObj(self):
# Handy reference
return APP().torRelaySettingsWidget.StorageObj
def launch(self, proxyServer):
self.torRelay = subprocess.Popen(
['tor', '-f', '-'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
torConfig = (
f'SocksPort {self.torRelayStorageObj.get("socksTunnelPort", DEFAULT_TOR_SOCKS_PORT)}\n'
f'HTTPTunnelPort {self.torRelayStorageObj.get("httpsTunnelPort", DEFAULT_TOR_HTTPS_PORT)}\n'
f'Log {self.torRelayStorageObj.get("logLevel", "notice")} stdout\n'
f'DataDirectory {DATA_DIR / "tordata"}\n'
)
if proxyServer:
logger.info(f'{TorRelay.name()} uses proxy server {proxyServer}')
# Use proxy
self.torRelay.stdin.write(f'{torConfig}HTTPSProxy {proxyServer}\n'.encode())
else:
self.torRelay.stdin.write(torConfig.encode())
self.torRelay.stdin.close()
self.startDaemonThread(self.torRelay.stdout)
# Begin get message
self.startTimer()
@QtCore.Slot()
def processLine(self):
line = self.getLineNoWait()
if line:
APP().torViewerWidget.textBrowser.append(line)
match = TorRelayStarter.BOOTSTRAP_STATUS.search(line)
if match:
percentage = int(match.group(1))
self.bootstrapPercentage = percentage
logger.info(f'{TorRelay.name()} bootstrapped {percentage}%')
class TorRelay(Core):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.starter = TorRelayStarter()
@staticmethod
def checkIfExists():
try:
subprocess.run(
['tor', '--version'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
except Exception:
# Any non-exit exceptions
return False
else:
return True
@staticmethod
def name():
return 'Tor Relay'
@staticmethod
def version():
try:
result = subprocess.run(
['tor', '--version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
)
# First line, 3rd param...
return result.stdout.decode().split('\n')[0].split()[2]
except Exception:
# Any non-exit exceptions
return '0.0.0'
@property
def bootstrapPercentage(self):
return self.starter.bootstrapPercentage
def start(self, *args, **kwargs):
logger.info(f'{self.name()} {self.version()} started')
self.starter.launch(kwargs.get('proxyServer', ''))
def stop(self):
if isinstance(self.starter.torRelay, subprocess.Popen):
# Stop timer
self.starter.stopTimer()
# Terminated by signal SIGTERM
self.starter.torRelay.send_signal(signal.SIGTERM)
exitcode = self.starter.torRelay.wait()
logger.info(f'{self.name()} terminated with exitcode {exitcode}')
self.starter.bootstrapPercentage = 0
# Reset relay
self.starter.torRelay = None
+7
View File
@@ -38,6 +38,13 @@ PROXY_SERVER_BYPASS = (
'172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*'
)
TOR_FAQ_URL = 'https://support.torproject.org/faq/'
TOR_FAQ_LABEL = f'Tor FAQ: <a href=\"{TOR_FAQ_URL}\">{TOR_FAQ_URL}</a>'
# Avoid standard Tor port in case of running Tor services
DEFAULT_TOR_SOCKS_PORT = 9048
DEFAULT_TOR_HTTPS_PORT = 9047
class Color:
LIGHT_RED_ = '#FF7276'
+2
View File
@@ -65,6 +65,8 @@ SUPPORTED_SETTINGS = (
Settings('Configuration'),
# User Activated Server Index
Settings('ActivatedItemIndex'),
# User Tor Relay Settings,
Settings('TorRelaySettings'),
# Server Widget Window Size
Settings('ServerWidgetWindowSize'),
# Routes Widget Window Size
+75 -10
View File
@@ -1,5 +1,6 @@
from Furious.Core.Core import XrayCore, Hysteria
from Furious.Utility.Constants import APP, APPLICATION_NAME
from Furious.Core.TorRelay import TorRelay
from Furious.Utility.Constants import APP, APPLICATION_NAME, TOR_FAQ_LABEL
class Translatable:
@@ -87,6 +88,9 @@ NO_TRANSLATION = [
'TLS',
XrayCore.name(),
Hysteria.name(),
TOR_FAQ_LABEL,
'socks',
'http',
]
NO_TRANSLATION_DICT = {key: True for key in NO_TRANSLATION}
@@ -133,6 +137,11 @@ TRANSLATION = {
'ZH': '绕过伊朗',
'TW': '繞過伊朗',
},
'Route My Traffic Through Tor': {
'ES': 'Enrutar Mi Tráfico a Través de Tor',
'ZH': '通过Tor路由我的流量',
'TW': '通過Tor路由我的流量',
},
'Global': {
'ES': 'Global',
'ZH': '全球',
@@ -143,11 +152,6 @@ TRANSLATION = {
'ZH': '自定义',
'TW': '自定義',
},
'Edit Routing...': {
'ES': 'Editar Enrutamiento...',
'ZH': '编辑路由...',
'TW': '編輯路由...',
},
'Import': {
'ES': 'Importar',
'ZH': '导入',
@@ -193,15 +197,30 @@ TRANSLATION = {
'ZH': '在编辑器中显示制表符和空格',
'TW': '在編輯器中顯示制表符和空格',
},
'Routing Settings...': {
'ES': 'Ajustes de Enrutamiento...',
'ZH': '路由设置...',
'TW': '路由設置...',
},
'Tor Relay Settings...': {
'ES': 'Ajustes de Tor Relay...',
'ZH': 'Tor Relay设置...',
'TW': 'Tor Relay設置...',
},
'Help': {
'ES': 'Ayuda',
'ZH': '帮助',
'TW': '幫助',
},
'Show Log...': {
'ES': 'Mostrar Registro...',
'ZH': '显示日志...',
'TW': '顯示日志...',
f'Show {APPLICATION_NAME} Log...': {
'ES': f'Mostrar Registro de {APPLICATION_NAME}...',
'ZH': f'显示{APPLICATION_NAME}日志...',
'TW': f'顯示{APPLICATION_NAME}日志...',
},
f'Show {TorRelay.name()} Log...': {
'ES': f'Mostrar Registro de {TorRelay.name()}...',
'ZH': f'显示{TorRelay.name()}日志...',
'TW': f'顯示{TorRelay.name()}日志...',
},
'Check For Updates': {
'ES': 'Comprobar Actualizaciones',
@@ -771,6 +790,42 @@ TRANSLATION = {
'ZH': '日志查看器',
'TW': '日志查看器',
},
# Tor Relay Settings
'Tor Relay Settings': {
'ES': 'Ajustes de Tor Relay',
'ZH': 'Tor Relay设置',
'TW': 'Tor Relay設置',
},
'Tunnel Port': {
'ES': 'Puerto de Túnel',
'ZH': '隧道端口',
'TW': '隧道端口',
},
'Proxy': {
'ES': 'Proxy',
'ZH': '代理',
'TW': '代理',
},
'Other': {
'ES': 'Otro',
'ZH': '其它',
'TW': '其它',
},
'Use Proxy': {
'ES': 'Usa Proxy',
'ZH': '使用代理',
'TW': '使用代理',
},
'Log Level': {
'ES': 'Nivel de Registro',
'ZH': '日志级别',
'TW': '日志級別',
},
'Relay Establish Timeout(seconds)': {
'ES': 'Establecer Tiempo de Espera de Relay(segundos)',
'ZH': 'Relay建立超时(秒)',
'TW': 'Relay建立超時(秒)',
},
# Check For Updates
'Check for updates failed.': {
'ES': 'Error al comprobar actualizaciones.',
@@ -838,6 +893,16 @@ TRANSLATION = {
'ZH': '路由已更改:',
'TW': '路由已更改:',
},
f'{TorRelay.name()} establish timeout': {
'ES': f'Establecer tiempo de espera de {TorRelay.name()}',
'ZH': f'{TorRelay.name()}建立超时',
'TW': f'{TorRelay.name()}建立超時',
},
'Cannot find Tor CLI in PATH': {
'ES': 'No se puede encontrar Tor CLI en PATH',
'ZH': '在PATH中找不到Tor CLI',
'TW': '在PATH中找不到Tor CLI',
},
# Errors
f'{APPLICATION_NAME} is not be able to run on this operating system.': {
'ES': f'{APPLICATION_NAME} no puede ejecutarse en este sistema operativo.',
+84
View File
@@ -1,13 +1,16 @@
from Furious.Gui.Icon import Icon
from Furious.Utility.Constants import APP, PLATFORM, ROOT_DIR
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication
import os
import copy
import ujson
import queue
import logging
import pybase64
import threading
import functools
import subprocess
@@ -118,6 +121,87 @@ class RoutesStorage:
APP().CustomRouting = ''
class TorRelaySettingsStorage:
EMPTY_OBJECT = {
'socksTunnelPort': 9050,
'httpsTunnelPort': 9049,
'useProxy': True,
'logLevel': 'notice',
'relayEstablishTimeout': 15,
}
@staticmethod
def init():
return copy.deepcopy(TorRelaySettingsStorage.EMPTY_OBJECT)
@staticmethod
def sync(ob=None):
if ob is None:
# Object is up-to-date
APP().TorRelaySettings = TorRelaySettingsStorage.toStorage(
APP().torRelaySettingsWidget.StorageObj
)
else:
# Object is up-to-date
APP().TorRelaySettings = TorRelaySettingsStorage.toStorage(ob)
@staticmethod
def toObject(st):
if not st:
# Server storage does not exist, or is empty
return TorRelaySettingsStorage.init()
return ujson.loads(Base64Encoder.decode(st))
@staticmethod
def toStorage(ob):
return Base64Encoder.encode(
ujson.dumps(ob, ensure_ascii=False, escape_forward_slashes=False).encode()
)
@staticmethod
def clear():
APP().TorRelaySettings = ''
class AsyncSubprocessMessage:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.msgQueue = queue.Queue()
self.msgTimer = QtCore.QTimer()
self.daemonThread = None
def connectTimeoutCallback(self, callback):
self.msgTimer.timeout.connect(callback)
def startDaemonThread(self, stdout):
def enqueue(msgQueue):
for line in iter(stdout.readline, b''):
msgQueue.put(line.decode('utf-8', 'replace').strip())
stdout.close()
self.daemonThread = threading.Thread(
target=enqueue, args=(self.msgQueue,), daemon=True
)
self.daemonThread.start()
def startTimer(self, msec=1):
self.msgTimer.start(msec)
def stopTimer(self):
self.msgTimer.stop()
def getLineNoWait(self):
try:
return self.msgQueue.get_nowait()
except queue.Empty:
# Queue is empty
return ''
class Switch:
OFF = '0'
ON_ = '1'
+8 -1
View File
@@ -1,7 +1,8 @@
from Furious.Widget.SystemTrayIcon import SystemTrayIcon
from Furious.Widget.EditConfiguration import EditConfigurationWidget
from Furious.Widget.EditRouting import EditRoutingWidget
from Furious.Widget.LogViewer import LogViewerWidget
from Furious.Widget.LogViewer import LogViewerWidget, TorViewerWidget
from Furious.Widget.TorRelaySettings import TorRelaySettingsWidget
from Furious.Utility.Constants import (
APPLICATION_NAME,
APPLICATION_VERSION,
@@ -142,6 +143,8 @@ class Application(SingletonApplication):
self.customFontName = ''
self.logViewerWidget = None
self.torViewerWidget = None
self.logViewerHandle = None
self.logStreamHandle = None
@@ -150,6 +153,7 @@ class Application(SingletonApplication):
self.ServerWidget = None
self.RoutesWidget = None
self.torRelaySettingsWidget = None
def __getattr__(self, key):
try:
@@ -178,6 +182,8 @@ class Application(SingletonApplication):
def configureLogging(self):
self.logViewerWidget = LogViewerWidget()
self.torViewerWidget = TorViewerWidget()
self.logViewerHandle = LogViewerHandle(self.logViewerWidget.textBrowser)
self.logStreamHandle = logging.StreamHandler()
@@ -247,6 +253,7 @@ class Application(SingletonApplication):
self.ServerWidget = EditConfigurationWidget()
self.RoutesWidget = EditRoutingWidget()
self.torRelaySettingsWidget = TorRelaySettingsWidget()
self.tray = SystemTrayIcon()
self.tray.show()
+12 -17
View File
@@ -1,3 +1,4 @@
from Furious.Core.TorRelay import TorRelay
from Furious.Action.Import import ImportLinkAction, ImportJSONAction
from Furious.Action.Export import ExportLinkAction, ExportJSONAction, ExportQRCodeAction
from Furious.Core.Intellisense import Intellisense
@@ -600,22 +601,22 @@ class ZoomOutAction(Action):
self.parent().plainTextEdit.zoomOut()
class RoutingAction(Action):
def __init__(self, **kwargs):
super().__init__(_('Edit Routing...'), **kwargs)
def triggeredCallback(self, checked):
APP().RoutesWidget.show()
class ShowLogAction(Action):
def __init__(self, **kwargs):
super().__init__(_('Show Log...'), **kwargs)
super().__init__(_(f'Show {APPLICATION_NAME} Log...'), **kwargs)
def triggeredCallback(self, checked):
APP().logViewerWidget.showMaximized()
class ShowTorRelayLogAction(Action):
def __init__(self, **kwargs):
super().__init__(_(f'Show {TorRelay.name()} Log...'), **kwargs)
def triggeredCallback(self, checked):
APP().torViewerWidget.showMaximized()
class WhetherToUpdateInfoBox(MessageBox):
def __init__(self, version='0.0.0', *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1799,13 +1800,6 @@ class EditConfigurationWidget(MainWindow):
],
}
routingMenu = {
'name': 'Routing',
'actions': [
RoutingAction(parent=self),
],
}
viewMenu = {
'name': 'View',
'actions': [
@@ -1818,6 +1812,7 @@ class EditConfigurationWidget(MainWindow):
'name': 'Help',
'actions': [
ShowLogAction(parent=self),
ShowTorRelayLogAction(parent=self),
Seperator(),
CheckForUpdatesAction(parent=self),
AboutAction(parent=self),
@@ -1825,7 +1820,7 @@ class EditConfigurationWidget(MainWindow):
}
# Menus
for menuDict in (fileMenu, editMenu, routingMenu, viewMenu, helpMenu):
for menuDict in (fileMenu, editMenu, viewMenu, helpMenu):
menuName = menuDict['name']
menuObjName = f'_{menuName}Menu'
menu = Menu(*menuDict['actions'], title=_(menuName), parent=self.menuBar())
+25 -1
View File
@@ -30,6 +30,8 @@ from Furious.Utility.Constants import (
APPLICATION_NAME,
GOLDEN_RATIO,
DATA_DIR,
TOR_FAQ_URL,
TOR_FAQ_LABEL,
Color,
)
from Furious.Utility.Utility import (
@@ -141,6 +143,20 @@ BUILTIN_ROUTING_TEXT = {
escape_forward_slashes=False,
),
},
'Route My Traffic Through Tor': {
XrayCore.name(): (
f'# Xray-core Routing rules\n'
f'# \n'
f'# This rule is read-only.\n'
f'# \n'
f'# Use Tor to achieve better anonymity, but {APPLICATION_NAME}\n'
f'# never claims nor guarantees *FULL* anonymity.\n'
f'# \n'
f'# See more information:\n'
f'# \n'
f'# {TOR_FAQ_URL}\n\n'
),
},
'Global': {
XrayCore.name(): (
f'# Xray-core Routing rules\n'
@@ -192,6 +208,9 @@ ROUTING_HINT = {
'Bypass Iran': {
Hysteria.name(): '',
},
'Route My Traffic Through Tor': {
Hysteria.name(): TOR_FAQ_LABEL,
},
'Global': {
Hysteria.name(): HYSTERIA_ROUTING_HINT,
},
@@ -441,7 +460,7 @@ class EditRoutingAction(Action):
if dialog.isBuiltin:
# Built-in. Nothing to do
logger.info(
f'built-in routing dialog {BUILTIN_ROUTING[index]} called exec. Nothing to do'
f'built-in routing dialog \'{BUILTIN_ROUTING[index]}\' called exec. Nothing to do'
)
continue
@@ -729,6 +748,11 @@ class RoutingDialog(Translatable, SupportConnectedCallback, QDialog):
if hint:
self.hint = QLabel(hint)
self.hint.setTextFormat(QtCore.Qt.TextFormat.RichText)
self.hint.setTextInteractionFlags(
QtCore.Qt.TextInteractionFlag.TextBrowserInteraction
)
self.hint.setOpenExternalLinks(True)
layout.addRow(self.hint)
else:
+210
View File
@@ -0,0 +1,210 @@
from Furious.Widget.Widget import GroupBox, Label
from Furious.Utility.Constants import (
APP,
DEFAULT_TOR_SOCKS_PORT,
DEFAULT_TOR_HTTPS_PORT,
)
from Furious.Utility.Utility import (
StateContext,
SupportConnectedCallback,
TorRelaySettingsStorage,
bootstrapIcon,
moveToCenter,
)
from Furious.Utility.Translator import Translatable, gettext as _
from PySide6 import QtCore
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QGridLayout,
QSpinBox,
QWidget,
)
class TorRelaySettingsWidget(Translatable, SupportConnectedCallback, QDialog):
TOR_LOG_LEVEL = ['err', 'warn', 'notice', 'info', 'debug']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Tor Relay Settings'))
self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg'))
try:
self.StorageObj = TorRelaySettingsStorage.toObject(APP().TorRelaySettings)
except Exception:
# Any non-exit exceptions
self.StorageObj = TorRelaySettingsStorage.init()
# Clear it
TorRelaySettingsStorage.clear()
self.endpointGroupBox = GroupBox(_('Tunnel Port'), parent=self)
self.useProxyGroupBox = GroupBox(_('Proxy'), parent=self)
self.torOtherGroupBox = GroupBox(_('Other'), parent=self)
# Begin endpoint groupbox
self.socksTunnelLabel = Label(_('socks'), parent=self)
self.httpsTunnelLabel = Label(_('http'), parent=self)
self.socksTunnelSpinBox = QSpinBox(parent=self)
self.socksTunnelSpinBox.setMinimum(0)
self.socksTunnelSpinBox.setMaximum(65535)
self.httpsTunnelSpinBox = QSpinBox(parent=self)
self.httpsTunnelSpinBox.setMinimum(0)
self.httpsTunnelSpinBox.setMaximum(65535)
self.socksTunnelWidget = QWidget(parent=self)
self.socksTunnelLayout = QFormLayout(parent=self.socksTunnelWidget)
self.socksTunnelLayout.addRow(self.socksTunnelLabel, self.socksTunnelSpinBox)
self.socksTunnelWidget.setLayout(self.socksTunnelLayout)
self.httpsTunnelWidget = QWidget(parent=self)
self.httpsTunnelLayout = QFormLayout(parent=self.httpsTunnelWidget)
self.httpsTunnelLayout.addRow(self.httpsTunnelLabel, self.httpsTunnelSpinBox)
self.httpsTunnelWidget.setLayout(self.httpsTunnelLayout)
self.endpointGroupBoxLayout = QGridLayout(parent=self.endpointGroupBox)
self.endpointGroupBoxLayout.addWidget(self.socksTunnelWidget, 0, 0)
self.endpointGroupBoxLayout.addWidget(self.httpsTunnelWidget, 0, 1)
self.endpointGroupBox.setLayout(self.endpointGroupBoxLayout)
# End endpoint groupbox
# Begin useProxy groupbox
self.useProxyLabel = Label(_('Use Proxy'), parent=self)
self.useProxyCheckBox = QCheckBox(parent=self)
self.useProxyWidget = QWidget(parent=self)
self.useProxyLayout = QFormLayout(parent=self.useProxyWidget)
self.useProxyLayout.addRow(self.useProxyLabel, self.useProxyCheckBox)
self.useProxyWidget.setLayout(self.useProxyLayout)
self.useProxyGroupBoxLayout = QGridLayout(parent=self.useProxyGroupBox)
self.useProxyGroupBoxLayout.addWidget(self.useProxyWidget, 0, 0)
self.useProxyGroupBox.setLayout(self.useProxyGroupBoxLayout)
# End useProxy groupbox
# Begin torOther groupbox
self.torLogLevelLabel = Label(_('Log Level'), parent=self)
self.torLogLevelComboBox = QComboBox(parent=self)
self.torLogLevelComboBox.addItems(TorRelaySettingsWidget.TOR_LOG_LEVEL)
self.torRelayTimeoutLabel = Label(
_('Relay Establish Timeout(seconds)'), parent=self
)
self.torRelayTimeoutSpinBox = QSpinBox(parent=self)
self.torRelayTimeoutSpinBox.setMinimum(0)
self.torRelayTimeoutSpinBox.setMaximum(3600)
self.torLogLevelWidget = QWidget(parent=self)
self.torLogLevelLayout = QFormLayout(parent=self.torLogLevelWidget)
self.torLogLevelLayout.addRow(self.torLogLevelLabel, self.torLogLevelComboBox)
self.torLogLevelWidget.setLayout(self.torLogLevelLayout)
self.torRelayTimeoutWidget = QWidget(parent=self)
self.torRelayTimeoutLayout = QFormLayout(parent=self.torRelayTimeoutWidget)
self.torRelayTimeoutLayout.addRow(
self.torRelayTimeoutLabel, self.torRelayTimeoutSpinBox
)
self.torRelayTimeoutWidget.setLayout(self.torRelayTimeoutLayout)
self.torOtherGroupBoxLayout = QGridLayout(parent=self.torOtherGroupBox)
self.torOtherGroupBoxLayout.addWidget(self.torLogLevelWidget, 0, 0)
self.torOtherGroupBoxLayout.addWidget(self.torRelayTimeoutWidget, 0, 1)
self.torOtherGroupBox.setLayout(self.torOtherGroupBoxLayout)
# End torOther groupbox
# Restore value
self.restoreValueFromObject()
# Dialog buttons
self.dialogBtns = QDialogButtonBox(
QtCore.Qt.Orientation.Horizontal, parent=self
)
self.dialogBtns.addButton(_('OK'), QDialogButtonBox.ButtonRole.AcceptRole)
self.dialogBtns.addButton(_('Cancel'), QDialogButtonBox.ButtonRole.RejectRole)
self.dialogBtns.accepted.connect(self.handleAccepted)
self.dialogBtns.rejected.connect(self.handleRejected)
# Central Widget
self.fakeCentralWidget = QWidget(parent=self)
self.layout = QGridLayout(parent=self.fakeCentralWidget)
self.layout.addWidget(self.endpointGroupBox, 0, 0)
self.layout.addWidget(self.useProxyGroupBox, 1, 0)
self.layout.addWidget(self.torOtherGroupBox, 2, 0)
self.layout.addWidget(self.dialogBtns, 3, 0)
self.setLayout(self.layout)
@QtCore.Slot()
def handleAccepted(self):
# Sync object
self.StorageObj['socksTunnelPort'] = self.socksTunnelSpinBox.value()
self.StorageObj['httpsTunnelPort'] = self.httpsTunnelSpinBox.value()
self.StorageObj['useProxy'] = self.useProxyCheckBox.isChecked()
self.StorageObj['logLevel'] = self.torLogLevelComboBox.currentText()
self.StorageObj['relayEstablishTimeout'] = self.torRelayTimeoutSpinBox.value()
# Sync it
TorRelaySettingsStorage.sync()
self.hide()
@QtCore.Slot()
def handleRejected(self):
self.restoreValueFromObject()
self.hide()
def restoreValueFromObject(self):
# Restore value
self.socksTunnelSpinBox.setValue(
self.StorageObj.get('socksTunnelPort', DEFAULT_TOR_SOCKS_PORT)
)
self.httpsTunnelSpinBox.setValue(
self.StorageObj.get('httpsTunnelPort', DEFAULT_TOR_HTTPS_PORT)
)
self.useProxyCheckBox.setChecked(self.StorageObj.get('useProxy', True))
self.torLogLevelComboBox.setCurrentText(
self.StorageObj.get('logLevel', 'notice')
)
self.torRelayTimeoutSpinBox.setValue(
self.StorageObj.get('relayEstablishTimeout', 15)
)
def closeEvent(self, event):
event.ignore()
self.restoreValueFromObject()
self.hide()
def connectedCallback(self):
self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg'))
def disconnectedCallback(self):
self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg'))
def retranslate(self):
with StateContext(self):
self.setWindowTitle(_(self.windowTitle()))
for button in self.dialogBtns.buttons():
button.setText(_(button.text()))
moveToCenter(self)
+20
View File
@@ -12,8 +12,10 @@ from Furious.Utility.Translator import Translatable, gettext as _
from PySide6 import QtCore
from PySide6.QtGui import QBrush, QColor, QFont, QTextCursor
from PySide6.QtWidgets import (
QGroupBox,
QHeaderView,
QInputDialog,
QLabel,
QLineEdit,
QListWidget,
QMainWindow,
@@ -29,6 +31,15 @@ from PySide6.QtWidgets import (
)
class GroupBox(Translatable, QGroupBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def retranslate(self):
with StateContext(self):
self.setTitle(_(self.title()))
class HeaderView(SupportConnectedCallback, QHeaderView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -50,6 +61,15 @@ class HeaderView(SupportConnectedCallback, QHeaderView):
)
class Label(Translatable, QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def retranslate(self):
with StateContext(self):
self.setText(_(self.text()))
class ListWidget(SupportConnectedCallback, QListWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)