mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-09 07:09:51 +03:00
Introduce extensible core plugin architecture
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
+12
-317
@@ -22,167 +22,23 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Qt import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
from Furious.Core.XrayCore import *
|
||||
from Furious.Core.Hysteria1 import *
|
||||
from Furious.Core.Hysteria2 import *
|
||||
from Furious.Core.Tun2socks import *
|
||||
|
||||
from typing import Callable, Tuple, Union
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
import tempfile
|
||||
import functools
|
||||
|
||||
__all__ = ['cleanRoutingRule', 'CoreManager']
|
||||
__all__ = ['CoreManager']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fixLogObjectPath(config: ConfigFactory, attr: str, value: str, log=True):
|
||||
"""Resolve and normalize one log-file path in a core configuration."""
|
||||
try:
|
||||
path = config['log'][attr]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
config['log'][attr] = path = ''
|
||||
|
||||
if not isinstance(path, str) and not isinstance(path, bytes):
|
||||
config['log'][attr] = path = ''
|
||||
|
||||
if path == '':
|
||||
if SystemRuntime.isPythonw() and ProcessOutputRedirector.TemporaryDir.isValid():
|
||||
# Redirect implementation for pythonw environment
|
||||
config['log'][attr] = ProcessOutputRedirector.TemporaryDir.filePath(value)
|
||||
else:
|
||||
# Relative path fails if booting on start up
|
||||
# on Windows, when packed using nuitka...
|
||||
|
||||
# Fix relative path if needed. User cannot feel this operation.
|
||||
config['log'][attr] = absolutePath(path)
|
||||
|
||||
result = config['log'][attr]
|
||||
|
||||
if result:
|
||||
try:
|
||||
# Create a new file
|
||||
with open(result, 'x', encoding='utf-8'):
|
||||
pass
|
||||
except FileExistsError:
|
||||
pass
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
|
||||
if log:
|
||||
logger.info(
|
||||
f'{XrayCore.name()}: {attr} log is specified as \'{path}\'. '
|
||||
f'Fixed to \'{result}\''
|
||||
)
|
||||
|
||||
|
||||
def cleanRoutingRule(rule: dict):
|
||||
"""Remove empty match fields from an Xray routing rule."""
|
||||
if not isinstance(rule, dict):
|
||||
return None
|
||||
|
||||
result = {
|
||||
'type': 'field',
|
||||
'outboundTag': str(rule.get('outboundTag', 'proxy')).strip() or 'proxy',
|
||||
}
|
||||
|
||||
for key in [
|
||||
'domain',
|
||||
'ip',
|
||||
'sourceIP',
|
||||
'localIP',
|
||||
'user',
|
||||
'protocol',
|
||||
'inboundTag',
|
||||
'process',
|
||||
]:
|
||||
value = rule.get(key, [])
|
||||
|
||||
if isinstance(value, list):
|
||||
value = list(str(item).strip() for item in value if str(item).strip())
|
||||
|
||||
if value:
|
||||
result[key] = value
|
||||
|
||||
for key in [
|
||||
'port',
|
||||
'sourcePort',
|
||||
'localPort',
|
||||
'network',
|
||||
'vlessRoute',
|
||||
'balancerTag',
|
||||
'ruleTag',
|
||||
]:
|
||||
value = str(rule.get(key, '')).strip()
|
||||
|
||||
if value:
|
||||
result[key] = value
|
||||
|
||||
if len(result) <= 2:
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def customRoutingObjectFromSettings(routing: str):
|
||||
"""Load a named custom routing object from persistent settings."""
|
||||
prefix = 'Custom:'
|
||||
|
||||
if not isinstance(routing, str) or not routing.startswith(prefix):
|
||||
return None
|
||||
|
||||
unique = routing[len(prefix) :]
|
||||
routingProfile = Storage.UserRoutings().get(unique)
|
||||
|
||||
if not isinstance(routingProfile, dict):
|
||||
return None
|
||||
|
||||
if not routingProfile.get('enabled', True):
|
||||
return None
|
||||
|
||||
domainStrategy = routingProfile.get('domainStrategy', 'AsIs')
|
||||
|
||||
if domainStrategy not in ['AsIs', 'IPIfNonMatch', 'IPOnDemand']:
|
||||
domainStrategy = 'AsIs'
|
||||
|
||||
rules = list(
|
||||
filter(
|
||||
lambda rule: rule is not None,
|
||||
list(cleanRoutingRule(rule) for rule in routingProfile.get('rules', [])),
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
'domainStrategy': domainStrategy,
|
||||
'domainMatcher': 'hybrid',
|
||||
'rules': rules,
|
||||
}
|
||||
|
||||
|
||||
def routingObjectHasDirectRule(routingObject: dict) -> bool:
|
||||
"""Return whether an Xray routing object contains a direct rule."""
|
||||
try:
|
||||
return any(
|
||||
rule.get('outboundTag') == 'direct'
|
||||
for rule in routingObject.get('rules', [])
|
||||
if isinstance(rule, dict)
|
||||
)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def getUserTUNSettings(*args, **kwargs):
|
||||
"""Return one user-defined TUN setting or its fallback value."""
|
||||
return Storage.UserTUNSettings().get(*args, **kwargs)
|
||||
@@ -221,7 +77,6 @@ class CoreManager(Mixins.CleanupOnExit):
|
||||
self.uniqueCleanup = False
|
||||
self.processesPool = list()
|
||||
|
||||
@functools.singledispatchmethod
|
||||
def _startCore(
|
||||
self,
|
||||
config,
|
||||
@@ -232,181 +87,21 @@ class CoreManager(Mixins.CleanupOnExit):
|
||||
log=True,
|
||||
**kwargs,
|
||||
) -> Tuple[Union[CoreProcessWorker, None], bool]:
|
||||
"""Return the start core value used by the core manager."""
|
||||
return None, False
|
||||
"""Start a configuration through the plugin that owns it."""
|
||||
plugin = getPluginRegistry().pluginForConfig(config)
|
||||
if plugin is None:
|
||||
return None, False
|
||||
|
||||
@_startCore.register(ConfigXray)
|
||||
def _(
|
||||
self,
|
||||
config,
|
||||
routing,
|
||||
exitCallback=None,
|
||||
msgCallback=None,
|
||||
proxyModeOnly=False,
|
||||
log=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Handle the registered singledispatch variant."""
|
||||
if config.get('log') is None or not isinstance(config['log'], dict):
|
||||
config['log'] = {
|
||||
'access': '',
|
||||
'error': '',
|
||||
'loglevel': 'warning',
|
||||
}
|
||||
|
||||
logRedirectValue = str(uuid.uuid4())
|
||||
|
||||
# Fix logObject
|
||||
for attr in ['access', 'error']:
|
||||
fixLogObjectPath(config, attr, logRedirectValue, log)
|
||||
|
||||
if routing == AppBuiltinRouting.BypassMainlandChina.value:
|
||||
# TUN Mode handling
|
||||
if not proxyModeOnly and SystemRuntime.isTUNMode():
|
||||
showMBoxDirectRulesNotAllowed()
|
||||
|
||||
return None, False
|
||||
|
||||
routingObject = {
|
||||
'domainStrategy': 'IPIfNonMatch',
|
||||
'domainMatcher': 'hybrid',
|
||||
'rules': [
|
||||
{
|
||||
'type': 'field',
|
||||
'domain': [
|
||||
'geosite:category-ads-all',
|
||||
],
|
||||
'outboundTag': 'block',
|
||||
},
|
||||
{
|
||||
'type': 'field',
|
||||
'domain': [
|
||||
'geosite:cn',
|
||||
],
|
||||
'outboundTag': 'direct',
|
||||
},
|
||||
{
|
||||
'type': 'field',
|
||||
'ip': [
|
||||
'geoip:private',
|
||||
'geoip:cn',
|
||||
],
|
||||
'outboundTag': 'direct',
|
||||
},
|
||||
{
|
||||
'type': 'field',
|
||||
'port': '0-65535',
|
||||
'outboundTag': 'proxy',
|
||||
},
|
||||
],
|
||||
}
|
||||
elif routing == AppBuiltinRouting.Global.value:
|
||||
routingObject = {}
|
||||
elif customRoutingObjectFromSettings(routing) is not None:
|
||||
routingObject = customRoutingObjectFromSettings(routing)
|
||||
|
||||
#
|
||||
# TODO: Warning: no check for custom routing
|
||||
#
|
||||
# if (
|
||||
# routingObjectHasDirectRule(routingObject)
|
||||
# and not proxyModeOnly
|
||||
# and SystemRuntime.isTUNMode()
|
||||
# ):
|
||||
# showMBoxDirectRulesNotAllowed()
|
||||
#
|
||||
# return None, False
|
||||
elif routing == AppBuiltinRouting.Custom.value:
|
||||
routingObject = config.get('routing', {})
|
||||
else:
|
||||
routingObject = {}
|
||||
|
||||
if log:
|
||||
logger.info(f'core {XrayCore.name()} configured')
|
||||
logger.info(f'routing is {routing}')
|
||||
logger.info(f'RoutingObject: {routingObject}')
|
||||
|
||||
config['routing'] = routingObject
|
||||
|
||||
process = XrayCore(exitCallback=exitCallback, msgCallback=msgCallback)
|
||||
success = process.start(config, **kwargs)
|
||||
|
||||
return process, success
|
||||
|
||||
@_startCore.register(ConfigHysteria1)
|
||||
def _(
|
||||
self,
|
||||
config,
|
||||
routing,
|
||||
exitCallback=None,
|
||||
msgCallback=None,
|
||||
proxyModeOnly=False,
|
||||
log=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Handle the registered singledispatch variant."""
|
||||
if routing == AppBuiltinRouting.BypassMainlandChina.value:
|
||||
# TUN Mode handling
|
||||
if not proxyModeOnly and SystemRuntime.isTUNMode():
|
||||
showMBoxDirectRulesNotAllowed()
|
||||
|
||||
return None, False
|
||||
|
||||
routingObject = {
|
||||
'rule': DATA_DIR / 'hysteria' / 'bypass-mainland-China.acl',
|
||||
'mmdb': DATA_DIR / 'hysteria' / 'country.mmdb',
|
||||
}
|
||||
elif routing == AppBuiltinRouting.Global.value:
|
||||
routingObject = {
|
||||
'rule': '',
|
||||
'mmdb': '',
|
||||
}
|
||||
elif routing == AppBuiltinRouting.Custom.value:
|
||||
routingObject = {
|
||||
'rule': config.get('acl', ''),
|
||||
'mmdb': config.get('mmdb', ''),
|
||||
}
|
||||
else:
|
||||
routingObject = {
|
||||
'rule': '',
|
||||
'mmdb': '',
|
||||
}
|
||||
|
||||
if log:
|
||||
logger.info(f'core {Hysteria1.name()} configured')
|
||||
logger.info(f'routing is {routing}')
|
||||
logger.info(f'RoutingObject: {routingObject}')
|
||||
|
||||
process = Hysteria1(exitCallback=exitCallback, msgCallback=msgCallback)
|
||||
success = process.start(
|
||||
return plugin.startCore(
|
||||
config,
|
||||
Hysteria1.rule(routingObject.get('rule', '')),
|
||||
Hysteria1.mmdb(routingObject.get('mmdb', '')),
|
||||
routing,
|
||||
exitCallback=exitCallback,
|
||||
msgCallback=msgCallback,
|
||||
proxyModeOnly=proxyModeOnly,
|
||||
log=log,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return process, success
|
||||
|
||||
@_startCore.register(ConfigHysteria2)
|
||||
def _(
|
||||
self,
|
||||
config,
|
||||
routing,
|
||||
exitCallback=None,
|
||||
msgCallback=None,
|
||||
proxyModeOnly=False,
|
||||
log=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Handle the registered singledispatch variant."""
|
||||
if log:
|
||||
logger.info(f'core {Hysteria2.name()} configured')
|
||||
|
||||
process = Hysteria2(exitCallback=exitCallback, msgCallback=msgCallback)
|
||||
success = process.start(config, **kwargs)
|
||||
|
||||
return process, success
|
||||
|
||||
@staticmethod
|
||||
def waitForTUNDeviceBroughtUp(func: Callable[[str], bool], deviceName: str) -> bool:
|
||||
"""Wait for the platform to expose the configured TUN device."""
|
||||
|
||||
+2
-157
@@ -1,160 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wrap the embedded Hysteria 1 core process."""
|
||||
"""Provide compatibility imports for the official Hysteria 1 core binding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
import time
|
||||
import logging
|
||||
import functools
|
||||
import multiprocessing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ['Hysteria1']
|
||||
|
||||
|
||||
def startHysteria1(jsonString, rule, mmdb, msgQueue: multiprocessing.Queue):
|
||||
"""Start hysteria1."""
|
||||
try:
|
||||
import hysteria
|
||||
except ImportError:
|
||||
# Fake running process
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
else:
|
||||
if versionToValue(hysteria.__version__) <= versionToValue('1.3.5'):
|
||||
redirect = False
|
||||
else:
|
||||
redirect = True
|
||||
|
||||
ProcessOutputRedirector.launch(
|
||||
msgQueue,
|
||||
functools.partial(hysteria.startFromJSON, jsonString, rule, mmdb),
|
||||
redirect,
|
||||
)
|
||||
|
||||
|
||||
class Hysteria1(CoreProcessWorker):
|
||||
"""Manage the embedded Hysteria 1 core subprocess."""
|
||||
|
||||
class ExitCode(Enum):
|
||||
"""Enumerate process exit codes."""
|
||||
|
||||
ConfigurationError = 23
|
||||
RemoteNetworkError = 3
|
||||
# Windows shutting down
|
||||
SystemShuttingDown = 0x40010004
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the Hysteria1."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def loadOptionalFile(pathLike, fileType: str):
|
||||
"""Load optional file."""
|
||||
if isinstance(pathLike, str) and pathLike == '':
|
||||
return ''
|
||||
|
||||
try:
|
||||
path = absolutePath(str(pathLike))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'invalid hysteria1 {fileType} path. Fall back to empty')
|
||||
|
||||
return ''
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as file:
|
||||
data = file.read()
|
||||
|
||||
logger.info(f'hysteria1 {fileType} \'{path}\' load success')
|
||||
|
||||
return data
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(
|
||||
f'hysteria1 {fileType} \'{path}\' load failed. {ex}. '
|
||||
f'Fall back to empty'
|
||||
)
|
||||
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def rule(rulePath):
|
||||
"""Return the rule value."""
|
||||
return Hysteria1.loadOptionalFile(rulePath, 'rule')
|
||||
|
||||
@staticmethod
|
||||
def mmdb(mmdbPath):
|
||||
"""Return the mmdb value."""
|
||||
return Hysteria1.loadOptionalFile(mmdbPath, 'mmdb')
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
"""Return the process implementation name."""
|
||||
return 'Hysteria1'
|
||||
|
||||
@staticmethod
|
||||
def version() -> str:
|
||||
"""Return the bundled core version."""
|
||||
try:
|
||||
import hysteria
|
||||
|
||||
return hysteria.__version__
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return '0.0.0'
|
||||
|
||||
def launchSpec(
|
||||
self, config: Union[str, dict], rule, mmdb, **kwargs
|
||||
) -> Union[CoreLaunchSpec, None]:
|
||||
"""Build the child-process launch specification."""
|
||||
param = self.toJSONString(config)
|
||||
|
||||
if not param:
|
||||
return None
|
||||
|
||||
return CoreLaunchSpec(
|
||||
target=startHysteria1,
|
||||
args=(
|
||||
param,
|
||||
rule,
|
||||
mmdb,
|
||||
self.msgQueue,
|
||||
),
|
||||
processKwargs=kwargs,
|
||||
)
|
||||
|
||||
def start(self, config: Union[str, dict], rule, mmdb, **kwargs) -> bool:
|
||||
"""Start the hysteria1."""
|
||||
launchSpec = self.launchSpec(config, rule, mmdb, **kwargs)
|
||||
|
||||
if launchSpec is None:
|
||||
return False
|
||||
|
||||
return self.startWithSpec(launchSpec)
|
||||
from Furious.Plugins.Official.Hysteria1.Core import *
|
||||
|
||||
+2
-114
@@ -1,117 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wrap the embedded Hysteria 2 core process."""
|
||||
"""Provide compatibility imports for the official Hysteria 2 core binding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
import time
|
||||
import logging
|
||||
import functools
|
||||
import multiprocessing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ['Hysteria2']
|
||||
|
||||
|
||||
def startHysteria2(jsonString: str, msgQueue: multiprocessing.Queue):
|
||||
"""Start hysteria2."""
|
||||
try:
|
||||
import hysteria2
|
||||
except ImportError:
|
||||
# Fake running process
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
else:
|
||||
if versionToValue(hysteria2.__version__) <= versionToValue('2.0.0.1'):
|
||||
redirect = False
|
||||
else:
|
||||
redirect = True
|
||||
|
||||
ProcessOutputRedirector.launch(
|
||||
msgQueue,
|
||||
functools.partial(hysteria2.startFromJSON, jsonString),
|
||||
redirect,
|
||||
)
|
||||
|
||||
|
||||
class Hysteria2(CoreProcessWorker):
|
||||
"""Manage the embedded Hysteria 2 core subprocess."""
|
||||
|
||||
class ExitCode(Enum):
|
||||
"""Enumerate process exit codes."""
|
||||
|
||||
ConfigurationError = 23
|
||||
# Windows: 4294967295. Darwin, Linux: 255 (-1)
|
||||
ServerStartFailure = 4294967295 if PLATFORM == 'Windows' else 255
|
||||
# Windows shutting down
|
||||
SystemShuttingDown = 0x40010004
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the Hysteria2."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
"""Return the process implementation name."""
|
||||
return 'Hysteria2'
|
||||
|
||||
@staticmethod
|
||||
def version() -> str:
|
||||
"""Return the bundled core version."""
|
||||
try:
|
||||
import hysteria2
|
||||
|
||||
return hysteria2.__version__
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return '0.0.0'
|
||||
|
||||
def launchSpec(
|
||||
self, config: Union[str, dict], **kwargs
|
||||
) -> Union[CoreLaunchSpec, None]:
|
||||
"""Build the child-process launch specification."""
|
||||
param = self.toJSONString(config)
|
||||
|
||||
if not param:
|
||||
return None
|
||||
|
||||
return CoreLaunchSpec(
|
||||
target=startHysteria2,
|
||||
args=(
|
||||
param,
|
||||
self.msgQueue,
|
||||
),
|
||||
processKwargs=kwargs,
|
||||
)
|
||||
|
||||
def start(self, config: Union[str, dict], **kwargs) -> bool:
|
||||
"""Start the hysteria2."""
|
||||
launchSpec = self.launchSpec(config, **kwargs)
|
||||
|
||||
if launchSpec is None:
|
||||
return False
|
||||
|
||||
return self.startWithSpec(launchSpec)
|
||||
from Furious.Plugins.Official.Hysteria2.Core import *
|
||||
|
||||
+2
-177
@@ -1,180 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wrap the embedded Xray core process."""
|
||||
"""Provide compatibility imports for the official Xray core binding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
import io
|
||||
import time
|
||||
import threading
|
||||
import functools
|
||||
import multiprocessing
|
||||
|
||||
__all__ = ['XrayCore']
|
||||
|
||||
|
||||
def startXrayCore(jsonString: str, msgQueue: multiprocessing.Queue):
|
||||
"""Start Xray core."""
|
||||
try:
|
||||
import xray
|
||||
except ImportError:
|
||||
# Fake running process
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
else:
|
||||
if versionToValue(xray.__version__) <= versionToValue('1.8.4'):
|
||||
redirect = False
|
||||
else:
|
||||
# Can be redirected
|
||||
redirect = True
|
||||
|
||||
if not SystemRuntime.isPythonw():
|
||||
return ProcessOutputRedirector.launch(
|
||||
msgQueue,
|
||||
functools.partial(xray.startFromJSON, jsonString),
|
||||
redirect,
|
||||
)
|
||||
|
||||
if not redirect:
|
||||
return xray.startFromJSON(jsonString)
|
||||
|
||||
try:
|
||||
jsonObject = UJSONEncoder.decode(jsonString)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
xray.startFromJSON(jsonString)
|
||||
else:
|
||||
loggingPath = list()
|
||||
fileStreams = list()
|
||||
|
||||
for loggingAttr in ['access', 'error']:
|
||||
try:
|
||||
path = jsonObject['log'][loggingAttr]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
continue
|
||||
|
||||
if path not in loggingPath:
|
||||
loggingPath.append(path)
|
||||
|
||||
try:
|
||||
stream = open(path, 'rb')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
else:
|
||||
stream.seek(0, io.SEEK_END)
|
||||
|
||||
fileStreams.append(stream)
|
||||
|
||||
def produceMsg():
|
||||
"""Forward one process-output message to the shared queue."""
|
||||
while True:
|
||||
for file in fileStreams:
|
||||
for line in iter(file.readline, b''):
|
||||
if line and not line.isspace():
|
||||
try:
|
||||
msgQueue.put_nowait(line.decode('utf-8', 'replace'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
|
||||
time.sleep(MsgQueue.MSG_PRODUCE_THRESHOLD / 1000)
|
||||
|
||||
try:
|
||||
if fileStreams:
|
||||
msgThread = threading.Thread(target=produceMsg, daemon=True)
|
||||
msgThread.start()
|
||||
|
||||
xray.startFromJSON(jsonString)
|
||||
finally:
|
||||
for stream in fileStreams:
|
||||
stream.close()
|
||||
|
||||
|
||||
class XrayCore(CoreProcessWorker):
|
||||
"""Manage the embedded Xray core subprocess."""
|
||||
|
||||
class ExitCode(Enum):
|
||||
"""Enumerate process exit codes."""
|
||||
|
||||
ConfigurationError = 23
|
||||
# Windows: 4294967295. Darwin, Linux: 255 (-1)
|
||||
ServerStartFailure = 4294967295 if PLATFORM == 'Windows' else 255
|
||||
# Windows shutting down
|
||||
SystemShuttingDown = 0x40010004
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the XrayCore."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
"""Return the process implementation name."""
|
||||
return 'Xray-core'
|
||||
|
||||
@staticmethod
|
||||
def version() -> str:
|
||||
"""Return the bundled core version."""
|
||||
try:
|
||||
import xray
|
||||
|
||||
return xray.__version__
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return '0.0.0'
|
||||
|
||||
def launchSpec(
|
||||
self, config: Union[str, ConfigFactory, dict], **kwargs
|
||||
) -> Union[CoreLaunchSpec, None]:
|
||||
"""Build the child-process launch specification."""
|
||||
param = self.toJSONString(config)
|
||||
|
||||
if not param:
|
||||
return None
|
||||
|
||||
return CoreLaunchSpec(
|
||||
target=startXrayCore,
|
||||
args=(
|
||||
param,
|
||||
self.msgQueue,
|
||||
),
|
||||
processKwargs=kwargs,
|
||||
)
|
||||
|
||||
def start(self, config: Union[str, ConfigFactory, dict], **kwargs) -> bool:
|
||||
"""Start the Xray core."""
|
||||
launchSpec = self.launchSpec(config, **kwargs)
|
||||
|
||||
if launchSpec is None:
|
||||
return False
|
||||
|
||||
return self.startWithSpec(launchSpec)
|
||||
from Furious.Plugins.Official.Xray.Core import *
|
||||
|
||||
@@ -19,8 +19,5 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .XrayCore import *
|
||||
from .Hysteria1 import *
|
||||
from .Hysteria2 import *
|
||||
from .Tun2socks import *
|
||||
from .CoreManager import *
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library.Encoder import *
|
||||
from Furious.Library.Configuration import configFactoryFromAny
|
||||
from Furious.Plugins.Configuration import configFactoryFromAny
|
||||
|
||||
__all__ = ['UserServers']
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Plugins.Configuration import *
|
||||
|
||||
from .Encoder import *
|
||||
from .Storage import *
|
||||
from .Configuration import *
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Define the public contract implemented by Furious core plugins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
__all__ = ['PLUGIN_API_VERSION', 'PluginProtocol', 'FuriousPlugin']
|
||||
|
||||
PLUGIN_API_VERSION = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PluginProtocol:
|
||||
"""Describe one server protocol contributed by a plugin."""
|
||||
|
||||
id: str
|
||||
displayName: str
|
||||
addActionText: str
|
||||
menuOrder: int = 0
|
||||
separatorBefore: bool = False
|
||||
|
||||
|
||||
class FuriousPlugin:
|
||||
"""Provide configuration, UI, and process hooks for one proxy core family."""
|
||||
|
||||
apiVersion = PLUGIN_API_VERSION
|
||||
pluginId = ''
|
||||
displayName = ''
|
||||
protocols = tuple()
|
||||
configurationTypes = tuple()
|
||||
coreTypes = tuple()
|
||||
|
||||
def configFromString(self, config: str, **kwargs):
|
||||
"""Construct a supported configuration from text or return ``None``."""
|
||||
return None
|
||||
|
||||
def configFromDict(self, config: dict, **kwargs):
|
||||
"""Construct a supported configuration mapping or return ``None``."""
|
||||
return None
|
||||
|
||||
def blankConfig(self, protocol, **kwargs):
|
||||
"""Construct a blank configuration for a contributed protocol."""
|
||||
return None
|
||||
|
||||
def createEditorForProtocol(self, protocol, parent=None, **kwargs):
|
||||
"""Create the configuration editor for a contributed protocol."""
|
||||
return None
|
||||
|
||||
def createEditorForConfig(self, config, parent=None, **kwargs):
|
||||
"""Create the configuration editor for a plugin configuration."""
|
||||
return self.createEditorForProtocol(config.itemProtocol, parent, **kwargs)
|
||||
|
||||
def createRoutingEditor(self, parent=None, **kwargs):
|
||||
"""Create an optional plugin-specific routing editor."""
|
||||
return None
|
||||
|
||||
def createMainWindowTools(self, parent=None, **kwargs):
|
||||
"""Return optional actions for the main window's Tools menu."""
|
||||
return tuple()
|
||||
|
||||
def routingChoices(self):
|
||||
"""Return optional ``(label, value)`` routing choices for the tray."""
|
||||
return tuple()
|
||||
|
||||
def configureEnvironment(self):
|
||||
"""Set optional process environment required by the plugin core."""
|
||||
|
||||
def startCore(
|
||||
self,
|
||||
config,
|
||||
routing,
|
||||
exitCallback=None,
|
||||
msgCallback=None,
|
||||
proxyModeOnly=False,
|
||||
log=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Start the plugin core and return ``(process, success)``."""
|
||||
return None, False
|
||||
|
||||
def prepareDownloadTest(self, config, port: int):
|
||||
"""Return a proxy-only configuration for a download-speed test."""
|
||||
return None
|
||||
|
||||
def coreVersions(self):
|
||||
"""Return core version strings that should be treated as versions in logs."""
|
||||
return tuple()
|
||||
|
||||
def logTimestampPatterns(self):
|
||||
"""Return regular expressions for timestamps emitted by the plugin core."""
|
||||
return tuple()
|
||||
|
||||
def coreExitMessage(self, core, exitcode: int):
|
||||
"""Return a user-facing message key for a plugin-specific exit code."""
|
||||
return None
|
||||
|
||||
def afterConnected(self, httpProxy=None):
|
||||
"""Perform optional plugin maintenance after a connection succeeds."""
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Dispatch configuration creation through the process-wide plugin registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Interface.ConfigFactory import ConfigFactory
|
||||
|
||||
from typing import Union
|
||||
|
||||
import ujson
|
||||
|
||||
__all__ = ['configFactoryFromDict', 'configFactoryFromAny', 'configFactoryBlank']
|
||||
|
||||
|
||||
def configFactoryFromDict(config: dict, **kwargs) -> ConfigFactory:
|
||||
"""Construct a configuration mapping through the first matching plugin."""
|
||||
if not isinstance(config, dict):
|
||||
return ConfigFactory()
|
||||
|
||||
from .Registry import getPluginRegistry
|
||||
|
||||
factory = getPluginRegistry().configFromDict(config, **kwargs)
|
||||
|
||||
return factory if factory is not None else ConfigFactory(config, **kwargs)
|
||||
|
||||
|
||||
def configFactoryFromAny(config: Union[str, dict], **kwargs) -> ConfigFactory:
|
||||
"""Construct configuration data through the first matching plugin."""
|
||||
if isinstance(config, str):
|
||||
from .Registry import getPluginRegistry
|
||||
|
||||
factory = getPluginRegistry().configFromString(config, **kwargs)
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
try:
|
||||
return configFactoryFromDict(ujson.loads(config), **kwargs)
|
||||
except Exception:
|
||||
return ConfigFactory(**kwargs)
|
||||
|
||||
if isinstance(config, dict):
|
||||
return configFactoryFromDict(config, **kwargs)
|
||||
|
||||
return ConfigFactory(**kwargs)
|
||||
|
||||
|
||||
def configFactoryBlank(protocol, **kwargs) -> ConfigFactory:
|
||||
"""Construct a blank configuration through the owning plugin."""
|
||||
from .Registry import getPluginRegistry
|
||||
|
||||
factory = getPluginRegistry().blankConfig(protocol, **kwargs)
|
||||
|
||||
return factory if factory is not None else ConfigFactory()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wrap the embedded Hysteria 1 core process."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
import time
|
||||
import logging
|
||||
import functools
|
||||
import multiprocessing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ['Hysteria1']
|
||||
|
||||
|
||||
def startHysteria1(jsonString, rule, mmdb, msgQueue: multiprocessing.Queue):
|
||||
"""Start hysteria1."""
|
||||
try:
|
||||
import hysteria
|
||||
except ImportError:
|
||||
# Fake running process
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
else:
|
||||
if versionToValue(hysteria.__version__) <= versionToValue('1.3.5'):
|
||||
redirect = False
|
||||
else:
|
||||
redirect = True
|
||||
|
||||
ProcessOutputRedirector.launch(
|
||||
msgQueue,
|
||||
functools.partial(hysteria.startFromJSON, jsonString, rule, mmdb),
|
||||
redirect,
|
||||
)
|
||||
|
||||
|
||||
class Hysteria1(CoreProcessWorker):
|
||||
"""Manage the embedded Hysteria 1 core subprocess."""
|
||||
|
||||
class ExitCode(Enum):
|
||||
"""Enumerate process exit codes."""
|
||||
|
||||
ConfigurationError = 23
|
||||
RemoteNetworkError = 3
|
||||
# Windows shutting down
|
||||
SystemShuttingDown = 0x40010004
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the Hysteria1."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def loadOptionalFile(pathLike, fileType: str):
|
||||
"""Load optional file."""
|
||||
if isinstance(pathLike, str) and pathLike == '':
|
||||
return ''
|
||||
|
||||
try:
|
||||
path = absolutePath(str(pathLike))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'invalid hysteria1 {fileType} path. Fall back to empty')
|
||||
|
||||
return ''
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as file:
|
||||
data = file.read()
|
||||
|
||||
logger.info(f'hysteria1 {fileType} \'{path}\' load success')
|
||||
|
||||
return data
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(
|
||||
f'hysteria1 {fileType} \'{path}\' load failed. {ex}. '
|
||||
f'Fall back to empty'
|
||||
)
|
||||
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def rule(rulePath):
|
||||
"""Return the rule value."""
|
||||
return Hysteria1.loadOptionalFile(rulePath, 'rule')
|
||||
|
||||
@staticmethod
|
||||
def mmdb(mmdbPath):
|
||||
"""Return the mmdb value."""
|
||||
return Hysteria1.loadOptionalFile(mmdbPath, 'mmdb')
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
"""Return the process implementation name."""
|
||||
return 'Hysteria1'
|
||||
|
||||
@staticmethod
|
||||
def version() -> str:
|
||||
"""Return the bundled core version."""
|
||||
try:
|
||||
import hysteria
|
||||
|
||||
return hysteria.__version__
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return '0.0.0'
|
||||
|
||||
def launchSpec(
|
||||
self, config: Union[str, dict], rule, mmdb, **kwargs
|
||||
) -> Union[CoreLaunchSpec, None]:
|
||||
"""Build the child-process launch specification."""
|
||||
param = self.toJSONString(config)
|
||||
|
||||
if not param:
|
||||
return None
|
||||
|
||||
return CoreLaunchSpec(
|
||||
target=startHysteria1,
|
||||
args=(
|
||||
param,
|
||||
rule,
|
||||
mmdb,
|
||||
self.msgQueue,
|
||||
),
|
||||
processKwargs=kwargs,
|
||||
)
|
||||
|
||||
def start(self, config: Union[str, dict], rule, mmdb, **kwargs) -> bool:
|
||||
"""Start the hysteria1."""
|
||||
launchSpec = self.launchSpec(config, rule, mmdb, **kwargs)
|
||||
|
||||
if launchSpec is None:
|
||||
return False
|
||||
|
||||
return self.startWithSpec(launchSpec)
|
||||
@@ -0,0 +1,386 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI hysteria1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiHysteria1']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiHy1ItemTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy1 item text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldValue = config.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
if newValue == '':
|
||||
# Remove field
|
||||
config.pop(self.key, None)
|
||||
else:
|
||||
config[self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setText(config.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy1ItemBasicProtocol(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI hy1 item basic protocol."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemBasicProtocol."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(['', 'udp', 'wechat-video', 'faketcp'])
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldProtocol = config.get('protocol', '')
|
||||
newProtocol = self.text()
|
||||
|
||||
if isinstance(oldProtocol, str):
|
||||
if newProtocol != oldProtocol:
|
||||
config['protocol'] = newProtocol
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['protocol'] = newProtocol
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setText(config.get('protocol', 'udp'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy1ItemSpeedUpMbps(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI hy1 item speed up mbps."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemSpeedUpMbps."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 1048576)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldUpMbps = config.get('up_mbps')
|
||||
newUpMbps = self.value()
|
||||
|
||||
if isinstance(oldUpMbps, int):
|
||||
if newUpMbps != oldUpMbps:
|
||||
config['up_mbps'] = newUpMbps
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['up_mbps'] = newUpMbps
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setValue(config.get('up_mbps'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(24)
|
||||
|
||||
|
||||
class GuiHy1ItemSpeedDownMbps(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI hy1 item speed down mbps."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemSpeedDownMbps."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 1048576)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldDownMbps = config.get('down_mbps')
|
||||
newDownMbps = self.value()
|
||||
|
||||
if isinstance(oldDownMbps, int):
|
||||
if newDownMbps != oldDownMbps:
|
||||
config['down_mbps'] = newDownMbps
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['down_mbps'] = newDownMbps
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setValue(config.get('down_mbps'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(96)
|
||||
|
||||
|
||||
class GuiHy1ItemTLSInsecure(GuiEditorItemTextCheckBox):
|
||||
"""Represent GUI hy1 item TLS insecure."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemTLSInsecure."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
try:
|
||||
oldChecked = config.get('insecure')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldChecked = False
|
||||
|
||||
newChecked = self.isChecked()
|
||||
|
||||
if newChecked:
|
||||
if oldChecked is not True:
|
||||
config['insecure'] = True
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if oldChecked is True:
|
||||
config['insecure'] = False
|
||||
|
||||
return True
|
||||
if oldChecked is False:
|
||||
return False
|
||||
|
||||
# Invalid value for insecure. Remove it
|
||||
config.pop('insecure', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
checked = config['insecure']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
checked = False
|
||||
|
||||
if not isinstance(checked, bool):
|
||||
checked = False
|
||||
|
||||
self.setChecked(checked)
|
||||
|
||||
|
||||
class GuiHy1ProjectWebsiteURL(AppQLabel):
|
||||
"""Represent GUI hy1 project website URL."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ProjectWebsiteURL."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWebsiteURL()
|
||||
self.linkActivated.connect(self.handleLinkActivated)
|
||||
|
||||
def setWebsiteURL(self):
|
||||
"""Set website URL."""
|
||||
self.setText(
|
||||
'<html><head/><body><p>'
|
||||
'<a href=\"https://v1.hysteria.network/\">'
|
||||
'<span style=\" text-decoration: underline; color:#007ad6;\">'
|
||||
+ _('Project Website')
|
||||
+ '</span></a></p></body></html>'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def handleLinkActivated(link: str):
|
||||
"""Handle link activated."""
|
||||
if QDesktopServices.openUrl(QtCore.QUrl(link)):
|
||||
logger.info(f'open link \'{link}\' success')
|
||||
else:
|
||||
logger.error(f'open link \'{link}\' failed')
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the GUI hy1 project website URL."""
|
||||
self.setWebsiteURL()
|
||||
|
||||
|
||||
class GuiHy1GroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy1 group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiHy1ItemTextInput(title=_('Server'), key='server'),
|
||||
GuiHy1ItemBasicProtocol(title=_('Protocol')),
|
||||
GuiHy1ItemTextInput(title='auth_str', translatable=False, key='auth_str'),
|
||||
GuiHy1ItemTextInput(title='obfs', translatable=False, key='obfs'),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy1GroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy1 group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy1GroupBoxSpeed(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy1 group box speed."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxSpeed."""
|
||||
super().__init__(_('Speed'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy1ItemSpeedUpMbps(title='up_mbps', translatable=False),
|
||||
GuiHy1ItemSpeedDownMbps(title='down_mbps', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy1GroupBoxTLS(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy1 group box TLS."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxTLS."""
|
||||
super().__init__('TLS', **kwargs)
|
||||
|
||||
self.translatable = False
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy1ItemTextInput(title='sni', translatable=False, key='server_name'),
|
||||
GuiHy1ItemTextInput(title='alpn', translatable=False, key='alpn'),
|
||||
GuiHy1ItemTextInput(title='ca', translatable=False, key='ca'),
|
||||
GuiHy1ItemTLSInsecure(title='insecure', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy1GroupBoxOther(GuiEditorItemFactory, AppQGroupBox):
|
||||
"""Represent GUI hy1 group box other."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxOther."""
|
||||
super().__init__(_('Other'), **kwargs)
|
||||
|
||||
self._website = GuiHy1ProjectWebsiteURL()
|
||||
|
||||
layout = QFormLayout()
|
||||
layout.addRow(self._website)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
pass
|
||||
|
||||
|
||||
class GuiHysteria1(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI hysteria1."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHysteria1."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Hysteria1.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiHy1GroupBoxBasic(),
|
||||
GuiHy1GroupBoxProxy(),
|
||||
GuiHy1GroupBoxSpeed(),
|
||||
GuiHy1GroupBoxTLS(),
|
||||
# GuiHy1GroupBoxOther(),
|
||||
]
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Integrate Hysteria 1 configuration, editor, routing, and startup."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Plugins.API import FuriousPlugin, PluginProtocol
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
BLANK_CONFIG_HYSTERIA1,
|
||||
ConfigHysteria1,
|
||||
)
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from .Core import Hysteria1
|
||||
|
||||
__all__ = ['Hysteria1Plugin']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Hysteria1Plugin(FuriousPlugin):
|
||||
"""Provide official Hysteria 1 support."""
|
||||
|
||||
pluginId = 'official.hysteria1'
|
||||
displayName = 'Hysteria1'
|
||||
protocols = (
|
||||
PluginProtocol(
|
||||
'hysteria1',
|
||||
'hysteria1',
|
||||
'Add Hysteria1 Server...',
|
||||
50,
|
||||
True,
|
||||
),
|
||||
)
|
||||
configurationTypes = (ConfigHysteria1,)
|
||||
coreTypes = (Hysteria1,)
|
||||
|
||||
def configFromString(self, config: str, **kwargs):
|
||||
"""Parse a Hysteria 1 share URI."""
|
||||
return (
|
||||
ConfigHysteria1(config, **kwargs)
|
||||
if config.startswith('hysteria://')
|
||||
else None
|
||||
)
|
||||
|
||||
def configFromDict(self, config: dict, **kwargs):
|
||||
"""Recognize Hysteria 1 configuration mappings."""
|
||||
if config.get('server') is None:
|
||||
return None
|
||||
|
||||
fields = (
|
||||
'protocol',
|
||||
'up_mbps',
|
||||
'down_mbps',
|
||||
'auth_str',
|
||||
'alpn',
|
||||
'server_name',
|
||||
'insecure',
|
||||
'recv_window_conn',
|
||||
'recv_window',
|
||||
'fast_open',
|
||||
'lazy_start',
|
||||
)
|
||||
if any(config.get(field) is not None for field in fields) or isinstance(
|
||||
config.get('obfs'), str
|
||||
):
|
||||
return ConfigHysteria1(config, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
def blankConfig(self, protocol, **kwargs):
|
||||
"""Construct a blank Hysteria 1 configuration."""
|
||||
return ConfigHysteria1(copy.deepcopy(BLANK_CONFIG_HYSTERIA1), **kwargs)
|
||||
|
||||
def createEditorForProtocol(self, protocol, parent=None, **kwargs):
|
||||
"""Create the Hysteria 1 editor."""
|
||||
from .GuiHysteria1 import GuiHysteria1
|
||||
|
||||
return GuiHysteria1(parent=parent, **kwargs)
|
||||
|
||||
def startCore(
|
||||
self,
|
||||
config,
|
||||
routing,
|
||||
exitCallback=None,
|
||||
msgCallback=None,
|
||||
proxyModeOnly=False,
|
||||
log=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Configure Hysteria 1 routing files and start its core."""
|
||||
if routing == AppBuiltinRouting.BypassMainlandChina.value:
|
||||
if not proxyModeOnly and SystemRuntime.isTUNMode():
|
||||
from Furious.Qt.QtWidgets import showMBoxDirectRulesNotAllowed
|
||||
|
||||
showMBoxDirectRulesNotAllowed()
|
||||
|
||||
return None, False
|
||||
|
||||
routingObject = {
|
||||
'rule': DATA_DIR / 'hysteria' / 'bypass-mainland-China.acl',
|
||||
'mmdb': DATA_DIR / 'hysteria' / 'country.mmdb',
|
||||
}
|
||||
elif routing == AppBuiltinRouting.Custom.value:
|
||||
routingObject = {
|
||||
'rule': config.get('acl', ''),
|
||||
'mmdb': config.get('mmdb', ''),
|
||||
}
|
||||
else:
|
||||
routingObject = {'rule': '', 'mmdb': ''}
|
||||
|
||||
if log:
|
||||
logger.info(f'core {Hysteria1.name()} configured')
|
||||
logger.info(f'routing is {routing}')
|
||||
logger.info(f'RoutingObject: {routingObject}')
|
||||
|
||||
process = Hysteria1(exitCallback=exitCallback, msgCallback=msgCallback)
|
||||
success = process.start(
|
||||
config,
|
||||
Hysteria1.rule(routingObject.get('rule', '')),
|
||||
Hysteria1.mmdb(routingObject.get('mmdb', '')),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return process, success
|
||||
|
||||
def prepareDownloadTest(self, config, port: int):
|
||||
"""Create a Hysteria 1 configuration with one local HTTP proxy."""
|
||||
configcopy = config.deepcopy()
|
||||
configcopy['http'] = {
|
||||
'listen': f'127.0.0.1:{port}',
|
||||
'timeout': 300,
|
||||
'disable_udp': False,
|
||||
}
|
||||
configcopy.pop('socks5', '')
|
||||
|
||||
return configcopy
|
||||
|
||||
def coreVersions(self):
|
||||
"""Return the bundled Hysteria 1 version."""
|
||||
return (Hysteria1.version(),)
|
||||
|
||||
def coreExitMessage(self, core, exitcode: int):
|
||||
"""Interpret the Hysteria 1 remote-network exit code."""
|
||||
if exitcode == Hysteria1.ExitCode.RemoteNetworkError.value:
|
||||
return 'Connection to server has been lost'
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Expose the official Hysteria 1 plugin implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Plugin import *
|
||||
@@ -0,0 +1,117 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wrap the embedded Hysteria 2 core process."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
import time
|
||||
import logging
|
||||
import functools
|
||||
import multiprocessing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ['Hysteria2']
|
||||
|
||||
|
||||
def startHysteria2(jsonString: str, msgQueue: multiprocessing.Queue):
|
||||
"""Start hysteria2."""
|
||||
try:
|
||||
import hysteria2
|
||||
except ImportError:
|
||||
# Fake running process
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
else:
|
||||
if versionToValue(hysteria2.__version__) <= versionToValue('2.0.0.1'):
|
||||
redirect = False
|
||||
else:
|
||||
redirect = True
|
||||
|
||||
ProcessOutputRedirector.launch(
|
||||
msgQueue,
|
||||
functools.partial(hysteria2.startFromJSON, jsonString),
|
||||
redirect,
|
||||
)
|
||||
|
||||
|
||||
class Hysteria2(CoreProcessWorker):
|
||||
"""Manage the embedded Hysteria 2 core subprocess."""
|
||||
|
||||
class ExitCode(Enum):
|
||||
"""Enumerate process exit codes."""
|
||||
|
||||
ConfigurationError = 23
|
||||
# Windows: 4294967295. Darwin, Linux: 255 (-1)
|
||||
ServerStartFailure = 4294967295 if PLATFORM == 'Windows' else 255
|
||||
# Windows shutting down
|
||||
SystemShuttingDown = 0x40010004
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the Hysteria2."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
"""Return the process implementation name."""
|
||||
return 'Hysteria2'
|
||||
|
||||
@staticmethod
|
||||
def version() -> str:
|
||||
"""Return the bundled core version."""
|
||||
try:
|
||||
import hysteria2
|
||||
|
||||
return hysteria2.__version__
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return '0.0.0'
|
||||
|
||||
def launchSpec(
|
||||
self, config: Union[str, dict], **kwargs
|
||||
) -> Union[CoreLaunchSpec, None]:
|
||||
"""Build the child-process launch specification."""
|
||||
param = self.toJSONString(config)
|
||||
|
||||
if not param:
|
||||
return None
|
||||
|
||||
return CoreLaunchSpec(
|
||||
target=startHysteria2,
|
||||
args=(
|
||||
param,
|
||||
self.msgQueue,
|
||||
),
|
||||
processKwargs=kwargs,
|
||||
)
|
||||
|
||||
def start(self, config: Union[str, dict], **kwargs) -> bool:
|
||||
"""Start the hysteria2."""
|
||||
launchSpec = self.launchSpec(config, **kwargs)
|
||||
|
||||
if launchSpec is None:
|
||||
return False
|
||||
|
||||
return self.startWithSpec(launchSpec)
|
||||
@@ -0,0 +1,891 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI hysteria2."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiHysteria2']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HY2_OBFS_TYPES = ['', 'salamander', 'gecko']
|
||||
|
||||
|
||||
class GuiHy2ItemBasicServer(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy2 item basic server."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemBasicServer."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldServer = config.get('server', '')
|
||||
newServer = self.text()
|
||||
|
||||
if isinstance(oldServer, str):
|
||||
if newServer != oldServer:
|
||||
config['server'] = newServer
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['server'] = newServer
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setText(config.get('server', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy2ItemBasicAuth(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy2 item basic auth."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemBasicAuth."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldAuth = config.get('auth')
|
||||
newAuth = self.text()
|
||||
|
||||
if newAuth == '':
|
||||
if oldAuth is None:
|
||||
return False
|
||||
else:
|
||||
config.pop('auth', None)
|
||||
|
||||
return True
|
||||
else:
|
||||
if isinstance(oldAuth, str):
|
||||
if newAuth != oldAuth:
|
||||
config['auth'] = newAuth
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['auth'] = newAuth
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setText(config.get('auth', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy2ItemBasicCongestionComboBox(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI hy2 item basic congestion combo box."""
|
||||
|
||||
CONGESTION_KEYS = ['type', 'bbrProfile']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemBasicCongestionComboBox."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
assert key in self.CONGESTION_KEYS
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
if key == 'type':
|
||||
# TODO: Future extension
|
||||
self.addItems(['', 'bbr', 'reno'])
|
||||
elif key == 'bbrProfile':
|
||||
# TODO: Future extension
|
||||
self.addItems(['', 'standard', 'conservative', 'aggressive'])
|
||||
else:
|
||||
# Should not reach here
|
||||
raise
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
newValue = self.text()
|
||||
|
||||
if newValue == '':
|
||||
congestion = config.get('congestion')
|
||||
|
||||
if isinstance(congestion, dict):
|
||||
if len(congestion) > 0:
|
||||
if congestion.get(self.key) is not None:
|
||||
oldValue = congestion[self.key]
|
||||
|
||||
congestion.pop(self.key, None)
|
||||
|
||||
if not isinstance(oldValue, str) or oldValue != '':
|
||||
modified = True
|
||||
else:
|
||||
modified = False
|
||||
else:
|
||||
modified = False
|
||||
|
||||
# TODO: Future extension
|
||||
anyValid = any(
|
||||
congestion.get(key, '') for key in self.CONGESTION_KEYS
|
||||
)
|
||||
|
||||
if not anyValid:
|
||||
config.pop('congestion', None)
|
||||
|
||||
return True
|
||||
else:
|
||||
return modified
|
||||
else:
|
||||
config.pop('congestion', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
else:
|
||||
config.pop('congestion', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
else:
|
||||
if not isinstance(config.get('congestion'), dict):
|
||||
config['congestion'] = {}
|
||||
|
||||
oldValue = config['congestion'].get(self.key, '')
|
||||
|
||||
if not isinstance(oldValue, str) or newValue != oldValue:
|
||||
config['congestion'][self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
value = config['congestion'][self.key]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
value = ''
|
||||
|
||||
self.setText(value)
|
||||
|
||||
|
||||
class GuiHy2ItemObfsType(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI hy2 item obfs type."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemObfsType."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(HY2_OBFS_TYPES)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
try:
|
||||
oldObfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldObfsType = ''
|
||||
|
||||
newObfsType = self.text()
|
||||
|
||||
if newObfsType == '':
|
||||
if config.get('obfs') is None:
|
||||
return False
|
||||
else:
|
||||
config.pop('obfs', None)
|
||||
|
||||
return True
|
||||
else:
|
||||
if not isinstance(oldObfsType, str) or newObfsType != oldObfsType:
|
||||
if not isinstance(config.get('obfs'), dict):
|
||||
config['obfs'] = {}
|
||||
|
||||
try:
|
||||
config['obfs']['type'] = newObfsType
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if isinstance(obfsType, str) and obfsType in HY2_OBFS_TYPES:
|
||||
self.setText(obfsType)
|
||||
else:
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy2ItemObfsPassword(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy2 item obfs password."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemObfsPassword."""
|
||||
self.obfsType = kwargs.pop('obfsType', 'salamander')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
newObfsPassword = self.text()
|
||||
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType != self.obfsType:
|
||||
return False
|
||||
|
||||
modified = False
|
||||
|
||||
if not isinstance(config.get('obfs'), dict):
|
||||
config['obfs'] = {}
|
||||
|
||||
modified = True
|
||||
|
||||
if not isinstance(config['obfs'].get(obfsType), dict):
|
||||
config['obfs'][obfsType] = {}
|
||||
|
||||
modified = True
|
||||
|
||||
try:
|
||||
oldObfsPassword = config['obfs'][obfsType]['password']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldObfsPassword = ''
|
||||
|
||||
if not isinstance(oldObfsPassword, str) or newObfsPassword != oldObfsPassword:
|
||||
config['obfs'][obfsType]['password'] = newObfsPassword
|
||||
|
||||
return True
|
||||
else:
|
||||
return modified
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType != self.obfsType:
|
||||
self.setText('')
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
obfsPassword = config['obfs'][obfsType]['password']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsPassword = ''
|
||||
|
||||
self.setText(obfsPassword)
|
||||
|
||||
|
||||
class GuiHy2ItemObfsPacketSize(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI hy2 item obfs packet size."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemObfsPacketSize."""
|
||||
self.obfsType, self.key, self.default = (
|
||||
kwargs.pop('obfsType', 'gecko'),
|
||||
kwargs.pop('key', ''),
|
||||
kwargs.pop('default', 0),
|
||||
)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setRange(1, 2048)
|
||||
self.setValue(self.default)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType != self.obfsType:
|
||||
return False
|
||||
|
||||
if not isinstance(config.get('obfs'), dict):
|
||||
config['obfs'] = {}
|
||||
|
||||
if not isinstance(config['obfs'].get(obfsType), dict):
|
||||
config['obfs'][obfsType] = {}
|
||||
|
||||
oldValue = config['obfs'][obfsType].get(self.key)
|
||||
newValue = self.value()
|
||||
|
||||
if not isinstance(oldValue, int) or newValue != oldValue:
|
||||
config['obfs'][obfsType][self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType != self.obfsType:
|
||||
self.setValue(self.default)
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
value = config['obfs'][obfsType][self.key]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
value = self.default
|
||||
|
||||
if not isinstance(value, int):
|
||||
value = self.default
|
||||
|
||||
self.setValue(value)
|
||||
|
||||
|
||||
class GuiHy2PageObfsXXX(GuiEditorWidgetQWidget):
|
||||
"""Represent GUI hy2 page obfs xxx."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2PageObfsXXX."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def setObfsTypeText(self, text: str):
|
||||
"""Set obfs type text."""
|
||||
obfsType = self._containers[0]
|
||||
|
||||
if isinstance(obfsType, GuiEditorItemTextComboBox):
|
||||
obfsType.setText(text)
|
||||
|
||||
def connectActivated(self, func):
|
||||
"""Connect activated."""
|
||||
obfsType = self._containers[0]
|
||||
|
||||
if isinstance(obfsType, GuiEditorItemTextComboBox):
|
||||
obfsType.connectActivated(func)
|
||||
|
||||
|
||||
class GuiHy2PageObfsEmpty(GuiHy2PageObfsXXX):
|
||||
"""Represent GUI hy2 page obfs empty."""
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2PageObfsSalamander(GuiHy2PageObfsXXX):
|
||||
"""Represent GUI hy2 page obfs salamander."""
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
|
||||
GuiHy2ItemObfsPassword(
|
||||
title='obfs-password',
|
||||
obfsType='salamander',
|
||||
translatable=False,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2PageObfsGecko(GuiHy2PageObfsXXX):
|
||||
"""Represent GUI hy2 page obfs gecko."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2PageObfsGecko."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.minPacketSizeItem = self._containers[2]
|
||||
self.maxPacketSizeItem = self._containers[3]
|
||||
self.minPacketSizeItem._input.valueChanged.connect(
|
||||
self.handleMinPacketSizeChanged
|
||||
)
|
||||
|
||||
self.handleMinPacketSizeChanged(self.minPacketSizeItem.value())
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
|
||||
GuiHy2ItemObfsPassword(
|
||||
title='obfs-password',
|
||||
obfsType='gecko',
|
||||
translatable=False,
|
||||
),
|
||||
GuiHy2ItemObfsPacketSize(
|
||||
title='minPacketSize',
|
||||
key='minPacketSize',
|
||||
default=512,
|
||||
translatable=False,
|
||||
),
|
||||
GuiHy2ItemObfsPacketSize(
|
||||
title='maxPacketSize',
|
||||
key='maxPacketSize',
|
||||
default=1200,
|
||||
translatable=False,
|
||||
),
|
||||
]
|
||||
|
||||
def handleMinPacketSizeChanged(self, value: int):
|
||||
"""Handle min packet size changed."""
|
||||
self.maxPacketSizeItem._input.setMinimum(value)
|
||||
|
||||
if self.maxPacketSizeItem.value() < value:
|
||||
self.maxPacketSizeItem.setValue(value)
|
||||
|
||||
|
||||
class GuiHy2ObfsPageStackedWidget(QStackedWidget):
|
||||
"""Provide the GUI hy2 obfs page stacked widget."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ObfsPageStackedWidget."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._pages = [
|
||||
GuiHy2PageObfsEmpty(),
|
||||
GuiHy2PageObfsSalamander(),
|
||||
GuiHy2PageObfsGecko(),
|
||||
]
|
||||
|
||||
for page in self._pages:
|
||||
self.addWidget(page)
|
||||
|
||||
def page(self, index: int) -> GuiHy2PageObfsXXX:
|
||||
"""Return the page value."""
|
||||
return self._pages[index]
|
||||
|
||||
def connectActivated(self, func):
|
||||
"""Connect activated."""
|
||||
for page in self._pages:
|
||||
page.connectActivated(func)
|
||||
|
||||
|
||||
class GuiHy2ItemTLSTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy2 item TLS text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemTLSTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
newValue = self.text()
|
||||
|
||||
if newValue == '':
|
||||
tls = config.get('tls')
|
||||
|
||||
if isinstance(tls, dict):
|
||||
if len(tls) > 0:
|
||||
if tls.get(self.key) is not None:
|
||||
oldValue = tls[self.key]
|
||||
|
||||
tls.pop(self.key, None)
|
||||
|
||||
if not isinstance(oldValue, str) or oldValue != '':
|
||||
modified = True
|
||||
else:
|
||||
# Modified silently
|
||||
modified = False
|
||||
else:
|
||||
modified = False
|
||||
|
||||
# TODO: Future extension
|
||||
anyValid = any(
|
||||
tls.get(key, '')
|
||||
for key in ['sni', 'insecure', 'pinSHA256', 'ca']
|
||||
)
|
||||
|
||||
if not anyValid:
|
||||
config.pop('tls', None)
|
||||
|
||||
return True
|
||||
else:
|
||||
return modified
|
||||
else:
|
||||
config.pop('tls', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
else:
|
||||
config.pop('tls', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
else:
|
||||
if not isinstance(config.get('tls'), dict):
|
||||
config['tls'] = {}
|
||||
|
||||
oldValue = config['tls'].get(self.key, '')
|
||||
|
||||
if not isinstance(oldValue, str) or newValue != oldValue:
|
||||
config['tls'][self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
value = config['tls'][self.key]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
value = ''
|
||||
|
||||
self.setText(value)
|
||||
|
||||
|
||||
class GuiHy2ItemTLSInsecure(GuiEditorItemTextCheckBox):
|
||||
"""Represent GUI hy2 item TLS insecure."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemTLSInsecure."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
try:
|
||||
oldChecked = config['tls']['insecure']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldChecked = False
|
||||
|
||||
newChecked = self.isChecked()
|
||||
|
||||
if newChecked:
|
||||
if not isinstance(config.get('tls'), dict):
|
||||
config['tls'] = {}
|
||||
|
||||
if oldChecked is not True:
|
||||
config['tls']['insecure'] = True
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if not isinstance(config.get('tls'), dict):
|
||||
config['tls'] = {}
|
||||
|
||||
if oldChecked is not False:
|
||||
config['tls']['insecure'] = False
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
checked = config['tls']['insecure']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
checked = False
|
||||
|
||||
if not isinstance(checked, bool):
|
||||
checked = False
|
||||
|
||||
self.setChecked(checked)
|
||||
|
||||
|
||||
class GuiHy2GroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy2 group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiHy2ItemBasicServer(title=_('Server')),
|
||||
GuiHy2ItemBasicAuth(title='auth', translatable=False),
|
||||
GuiHy2ItemBasicCongestionComboBox(
|
||||
title='congestion-type', key='type', translatable=False
|
||||
),
|
||||
GuiHy2ItemBasicCongestionComboBox(
|
||||
title='congestion-profile', key='bbrProfile', translatable=False
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2GroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy2 group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2GroupBoxObfs(GuiEditorItemFactory, AppQGroupBox):
|
||||
"""Represent GUI hy2 group box obfs."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxObfs."""
|
||||
translatable = kwargs.pop('translatable', False)
|
||||
|
||||
super().__init__('obfs', **kwargs, translatable=translatable)
|
||||
|
||||
self._config = ConfigFactory()
|
||||
|
||||
self._widget = GuiHy2ObfsPageStackedWidget()
|
||||
self._widget.connectActivated(self.handleActivated)
|
||||
|
||||
layout = QFormLayout()
|
||||
layout.addRow(self._widget)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def currentIndex(self) -> int:
|
||||
"""Return the current index value."""
|
||||
return self._widget.currentIndex()
|
||||
|
||||
def setCurrentIndex(self, index: int):
|
||||
"""Set current index."""
|
||||
self._widget.setCurrentIndex(index)
|
||||
|
||||
def page(self, index: int) -> GuiHy2PageObfsXXX:
|
||||
"""Return the page value."""
|
||||
return self._widget.page(index)
|
||||
|
||||
def handleActivated(self, index: int):
|
||||
"""Handle activated."""
|
||||
page = self.page(index)
|
||||
page.factoryToInput(self._config)
|
||||
page.setObfsTypeText(HY2_OBFS_TYPES[index])
|
||||
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldObfs = config.get('obfs')
|
||||
oldObfsType = ''
|
||||
|
||||
if isinstance(oldObfs, dict):
|
||||
oldObfsType = oldObfs.get('type', '')
|
||||
|
||||
newObfsType = HY2_OBFS_TYPES[self.currentIndex()]
|
||||
|
||||
if newObfsType == '':
|
||||
if isinstance(oldObfs, dict):
|
||||
config.pop('obfs', None)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
if newObfsType not in HY2_OBFS_TYPES:
|
||||
return False
|
||||
|
||||
if not isinstance(config.get('obfs'), dict):
|
||||
config['obfs'] = {}
|
||||
|
||||
modified = oldObfsType != newObfsType
|
||||
config['obfs']['type'] = newObfsType
|
||||
|
||||
for obfsType in HY2_OBFS_TYPES:
|
||||
if obfsType and obfsType != newObfsType:
|
||||
config['obfs'].pop(obfsType, None)
|
||||
|
||||
modified |= self.page(self.currentIndex()).inputToFactory(config)
|
||||
|
||||
return modified
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
self._config = config
|
||||
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType not in HY2_OBFS_TYPES:
|
||||
obfsType = ''
|
||||
|
||||
index = HY2_OBFS_TYPES.index(obfsType)
|
||||
|
||||
self.page(index).factoryToInput(config)
|
||||
self.page(index).setObfsTypeText(obfsType)
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
|
||||
class GuiHy2GroupBoxTLS(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy2 group box TLS."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxTLS."""
|
||||
super().__init__('TLS', **kwargs)
|
||||
|
||||
self.translatable = False
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy2ItemTLSTextInput(title='sni', key='sni', translatable=False),
|
||||
GuiHy2ItemTLSTextInput(
|
||||
title='pinSHA256', key='pinSHA256', translatable=False
|
||||
),
|
||||
GuiHy2ItemTLSTextInput(title='ca', key='ca', translatable=False),
|
||||
GuiHy2ItemTLSInsecure(title='insecure', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2ProjectWebsiteURL(AppQLabel):
|
||||
"""Represent GUI hy2 project website URL."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ProjectWebsiteURL."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWebsiteURL()
|
||||
self.linkActivated.connect(self.handleLinkActivated)
|
||||
|
||||
def setWebsiteURL(self):
|
||||
"""Set website URL."""
|
||||
self.setText(
|
||||
'<html><head/><body><p>'
|
||||
'<a href=\"https://v2.hysteria.network/\">'
|
||||
'<span style=\" text-decoration: underline; color:#007ad6;\">'
|
||||
+ _('Project Website')
|
||||
+ '</span></a></p></body></html>'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def handleLinkActivated(link: str):
|
||||
"""Handle link activated."""
|
||||
if QDesktopServices.openUrl(QtCore.QUrl(link)):
|
||||
logger.info(f'open link \'{link}\' success')
|
||||
else:
|
||||
logger.error(f'open link \'{link}\' failed')
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the GUI hy2 project website URL."""
|
||||
self.setWebsiteURL()
|
||||
|
||||
|
||||
class GuiHy2GroupBoxOther(GuiEditorItemFactory, AppQGroupBox):
|
||||
"""Represent GUI hy2 group box other."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxOther."""
|
||||
super().__init__(_('Other'), **kwargs)
|
||||
|
||||
self._website = GuiHy2ProjectWebsiteURL()
|
||||
|
||||
layout = QFormLayout()
|
||||
layout.addRow(self._website)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
pass
|
||||
|
||||
|
||||
class GuiHysteria2(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI hysteria2."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHysteria2."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Hysteria2.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiHy2GroupBoxBasic(),
|
||||
GuiHy2GroupBoxProxy(),
|
||||
GuiHy2GroupBoxObfs(),
|
||||
GuiHy2GroupBoxTLS(),
|
||||
# GuiHy2GroupBoxOther(),
|
||||
]
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Integrate Hysteria 2 configuration, editor, and process startup."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Plugins.API import FuriousPlugin, PluginProtocol
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
BLANK_CONFIG_HYSTERIA2,
|
||||
ConfigHysteria2,
|
||||
)
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from .Core import Hysteria2
|
||||
|
||||
__all__ = ['Hysteria2Plugin']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Hysteria2Plugin(FuriousPlugin):
|
||||
"""Provide official Hysteria 2 support."""
|
||||
|
||||
pluginId = 'official.hysteria2'
|
||||
displayName = 'Hysteria2'
|
||||
protocols = (
|
||||
PluginProtocol(
|
||||
'hysteria2',
|
||||
'hysteria2',
|
||||
'Add Hysteria2 Server...',
|
||||
60,
|
||||
),
|
||||
)
|
||||
configurationTypes = (ConfigHysteria2,)
|
||||
coreTypes = (Hysteria2,)
|
||||
|
||||
def configFromString(self, config: str, **kwargs):
|
||||
"""Parse a Hysteria 2 share URI."""
|
||||
if config.startswith(
|
||||
(
|
||||
'hy2://',
|
||||
'hysteria2://',
|
||||
'hysteria2+realm://',
|
||||
'hysteria2+realm+http://',
|
||||
)
|
||||
):
|
||||
return ConfigHysteria2(config, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
def configFromDict(self, config: dict, **kwargs):
|
||||
"""Recognize Hysteria 2 configuration mappings."""
|
||||
if config.get('server') is None:
|
||||
return None
|
||||
|
||||
fields = (
|
||||
'auth',
|
||||
'tls',
|
||||
'transport',
|
||||
'quic',
|
||||
'bandwidth',
|
||||
'tcpForwarding',
|
||||
'udpForwarding',
|
||||
'tcpTProxy',
|
||||
'udpTProxy',
|
||||
'fastOpen',
|
||||
'lazy',
|
||||
)
|
||||
if any(config.get(field) is not None for field in fields) or isinstance(
|
||||
config.get('obfs'), dict
|
||||
):
|
||||
return ConfigHysteria2(config, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
def blankConfig(self, protocol, **kwargs):
|
||||
"""Construct a blank Hysteria 2 configuration."""
|
||||
return ConfigHysteria2(copy.deepcopy(BLANK_CONFIG_HYSTERIA2), **kwargs)
|
||||
|
||||
def createEditorForProtocol(self, protocol, parent=None, **kwargs):
|
||||
"""Create the Hysteria 2 editor."""
|
||||
from .GuiHysteria2 import GuiHysteria2
|
||||
|
||||
return GuiHysteria2(parent=parent, **kwargs)
|
||||
|
||||
def startCore(
|
||||
self,
|
||||
config,
|
||||
routing,
|
||||
exitCallback=None,
|
||||
msgCallback=None,
|
||||
proxyModeOnly=False,
|
||||
log=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Start the Hysteria 2 core."""
|
||||
if log:
|
||||
logger.info(f'core {Hysteria2.name()} configured')
|
||||
|
||||
process = Hysteria2(exitCallback=exitCallback, msgCallback=msgCallback)
|
||||
|
||||
return process, process.start(config, **kwargs)
|
||||
|
||||
def prepareDownloadTest(self, config, port: int):
|
||||
"""Create a Hysteria 2 configuration with one local HTTP proxy."""
|
||||
configcopy = config.deepcopy()
|
||||
configcopy['http'] = {
|
||||
'listen': f'127.0.0.1:{port}',
|
||||
'timeout': 300,
|
||||
'disable_udp': False,
|
||||
}
|
||||
configcopy.pop('socks5', '')
|
||||
|
||||
return configcopy
|
||||
|
||||
def coreVersions(self):
|
||||
"""Return the bundled Hysteria 2 version."""
|
||||
return (Hysteria2.version(),)
|
||||
|
||||
def logTimestampPatterns(self):
|
||||
"""Return the timestamp format emitted by Hysteria 2."""
|
||||
return (r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})',)
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Expose the official Hysteria 2 plugin implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Plugin import *
|
||||
@@ -0,0 +1,180 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wrap the embedded Xray core process."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
import io
|
||||
import time
|
||||
import threading
|
||||
import functools
|
||||
import multiprocessing
|
||||
|
||||
__all__ = ['XrayCore']
|
||||
|
||||
|
||||
def startXrayCore(jsonString: str, msgQueue: multiprocessing.Queue):
|
||||
"""Start Xray core."""
|
||||
try:
|
||||
import xray
|
||||
except ImportError:
|
||||
# Fake running process
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
else:
|
||||
if versionToValue(xray.__version__) <= versionToValue('1.8.4'):
|
||||
redirect = False
|
||||
else:
|
||||
# Can be redirected
|
||||
redirect = True
|
||||
|
||||
if not SystemRuntime.isPythonw():
|
||||
return ProcessOutputRedirector.launch(
|
||||
msgQueue,
|
||||
functools.partial(xray.startFromJSON, jsonString),
|
||||
redirect,
|
||||
)
|
||||
|
||||
if not redirect:
|
||||
return xray.startFromJSON(jsonString)
|
||||
|
||||
try:
|
||||
jsonObject = UJSONEncoder.decode(jsonString)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
xray.startFromJSON(jsonString)
|
||||
else:
|
||||
loggingPath = list()
|
||||
fileStreams = list()
|
||||
|
||||
for loggingAttr in ['access', 'error']:
|
||||
try:
|
||||
path = jsonObject['log'][loggingAttr]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
continue
|
||||
|
||||
if path not in loggingPath:
|
||||
loggingPath.append(path)
|
||||
|
||||
try:
|
||||
stream = open(path, 'rb')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
else:
|
||||
stream.seek(0, io.SEEK_END)
|
||||
|
||||
fileStreams.append(stream)
|
||||
|
||||
def produceMsg():
|
||||
"""Forward one process-output message to the shared queue."""
|
||||
while True:
|
||||
for file in fileStreams:
|
||||
for line in iter(file.readline, b''):
|
||||
if line and not line.isspace():
|
||||
try:
|
||||
msgQueue.put_nowait(line.decode('utf-8', 'replace'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
|
||||
time.sleep(MsgQueue.MSG_PRODUCE_THRESHOLD / 1000)
|
||||
|
||||
try:
|
||||
if fileStreams:
|
||||
msgThread = threading.Thread(target=produceMsg, daemon=True)
|
||||
msgThread.start()
|
||||
|
||||
xray.startFromJSON(jsonString)
|
||||
finally:
|
||||
for stream in fileStreams:
|
||||
stream.close()
|
||||
|
||||
|
||||
class XrayCore(CoreProcessWorker):
|
||||
"""Manage the embedded Xray core subprocess."""
|
||||
|
||||
class ExitCode(Enum):
|
||||
"""Enumerate process exit codes."""
|
||||
|
||||
ConfigurationError = 23
|
||||
# Windows: 4294967295. Darwin, Linux: 255 (-1)
|
||||
ServerStartFailure = 4294967295 if PLATFORM == 'Windows' else 255
|
||||
# Windows shutting down
|
||||
SystemShuttingDown = 0x40010004
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the XrayCore."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
"""Return the process implementation name."""
|
||||
return 'Xray-core'
|
||||
|
||||
@staticmethod
|
||||
def version() -> str:
|
||||
"""Return the bundled core version."""
|
||||
try:
|
||||
import xray
|
||||
|
||||
return xray.__version__
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return '0.0.0'
|
||||
|
||||
def launchSpec(
|
||||
self, config: Union[str, ConfigFactory, dict], **kwargs
|
||||
) -> Union[CoreLaunchSpec, None]:
|
||||
"""Build the child-process launch specification."""
|
||||
param = self.toJSONString(config)
|
||||
|
||||
if not param:
|
||||
return None
|
||||
|
||||
return CoreLaunchSpec(
|
||||
target=startXrayCore,
|
||||
args=(
|
||||
param,
|
||||
self.msgQueue,
|
||||
),
|
||||
processKwargs=kwargs,
|
||||
)
|
||||
|
||||
def start(self, config: Union[str, ConfigFactory, dict], **kwargs) -> bool:
|
||||
"""Start the Xray core."""
|
||||
launchSpec = self.launchSpec(config, **kwargs)
|
||||
|
||||
if launchSpec is None:
|
||||
return False
|
||||
|
||||
return self.startWithSpec(launchSpec)
|
||||
@@ -0,0 +1,237 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI shadowsocks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiShadowsocks']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.Shadowsocks,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.Shadowsocks),
|
||||
)
|
||||
|
||||
|
||||
class GuiSSItemTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI ss item text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSSItemTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldValue = proxyOutboundServer.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
if newValue == '':
|
||||
# Remove field
|
||||
proxyOutboundServer.pop(self.key, None)
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiSSItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI ss item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSSItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiSSItemBasicMethod(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI ss item basic method."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSSItemBasicMethod."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(
|
||||
[
|
||||
'',
|
||||
'aes-128-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-poly1305',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
'none',
|
||||
'plain',
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
'2022-blake3-chacha20-poly1305',
|
||||
]
|
||||
)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldMethod = proxyOutboundServer.get('method', '')
|
||||
newMethod = self.text()
|
||||
|
||||
if isinstance(oldMethod, str):
|
||||
if newMethod != oldMethod:
|
||||
proxyOutboundServer['method'] = newMethod
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['method'] = newMethod
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get('method', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiSSGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI ss group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiSSGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiSSItemTextInput(title=_('Address'), key='address'),
|
||||
GuiSSItemBasicPort(title=_('Port')),
|
||||
GuiSSItemBasicMethod(title=_('Encryption')),
|
||||
GuiSSItemTextInput(title=_('Password'), key='password'),
|
||||
]
|
||||
|
||||
|
||||
class GuiSSGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI ss group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiSSGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiShadowsocks(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI shadowsocks."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiShadowsocks."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Shadowsocks.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiSSGroupBoxBasic(),
|
||||
GuiSSGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
@@ -0,0 +1,180 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI SOCKS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiSocks']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.Socks,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.Socks),
|
||||
)
|
||||
|
||||
|
||||
class GuiSocksItemTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI SOCKS item text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSocksItemTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldValue = proxyOutboundServer.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
if newValue == '':
|
||||
proxyOutboundServer.pop(self.key, None)
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiSocksItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI SOCKS item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSocksItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiSocksGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI SOCKS group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiSocksGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiSocksItemTextInput(title=_('Address'), key='address'),
|
||||
GuiSocksItemBasicPort(title=_('Port')),
|
||||
GuiSocksItemTextInput(title=_('Username'), key='user'),
|
||||
GuiSocksItemTextInput(title=_('Password'), key='pass'),
|
||||
]
|
||||
|
||||
|
||||
class GuiSocksGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI SOCKS group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiSocksGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiSocks(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI SOCKS."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSocks."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Socks.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiSocksGroupBoxBasic(),
|
||||
GuiSocksGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
@@ -0,0 +1,181 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI trojan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiTrojan']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.Trojan,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.Trojan),
|
||||
)
|
||||
|
||||
|
||||
class GuiTrojanItemTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI trojan item text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiTrojanItemTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldValue = proxyOutboundServer.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
if newValue == '':
|
||||
# Remove field
|
||||
proxyOutboundServer.pop(self.key, None)
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiTrojanItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI trojan item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiTrojanItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiTrojanGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI trojan group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiTrojanGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiTrojanItemTextInput(title=_('Address'), key='address'),
|
||||
GuiTrojanItemBasicPort(title=_('Port')),
|
||||
GuiTrojanItemTextInput(title=_('Password'), key='password'),
|
||||
]
|
||||
|
||||
|
||||
class GuiTrojanGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI trojan group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiTrojanGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiTrojan(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI trojan."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiTrojan."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Trojan.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiTrojanGroupBoxBasic(),
|
||||
GuiTrojanGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
@@ -0,0 +1,380 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI VLESS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import uuid
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiVLESS']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.VLESS,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.VLESS),
|
||||
)
|
||||
|
||||
getProxyOutboundUser = functools.partial(
|
||||
ConfigXray.getProxyOutboundUser,
|
||||
protocol=Protocol.VLESS,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.VLESS),
|
||||
)
|
||||
|
||||
|
||||
class GuiVLESSItemBasicAddress(GuiEditorItemTextInput):
|
||||
"""Represent GUI VLESS item basic address."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicAddress."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldAddress = proxyOutboundServer.get('address', '')
|
||||
newAddress = self.text()
|
||||
|
||||
if isinstance(oldAddress, str):
|
||||
if newAddress != oldAddress:
|
||||
if newAddress == '':
|
||||
# Remove field
|
||||
proxyOutboundServer.pop('address', None)
|
||||
else:
|
||||
proxyOutboundServer['address'] = newAddress
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['address'] = newAddress
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get('address', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVLESSItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI VLESS item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiVLESSItemBasicId(GuiEditorItemTextInput):
|
||||
"""Represent GUI VLESS item basic id."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicId."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.generateButton = AppQPushButton(_('Generate'))
|
||||
self.generateButton.clicked.connect(self.generateUUID)
|
||||
|
||||
self._widget = QWidget()
|
||||
|
||||
layout = QHBoxLayout(self._widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self._input)
|
||||
layout.addWidget(self.generateButton)
|
||||
|
||||
def generateUUID(self):
|
||||
"""Handle generate uuid for the GUI VLESS item basic id."""
|
||||
self.setText(str(uuid.uuid4()))
|
||||
|
||||
def widgets(self):
|
||||
"""Return the widgets owned by this editor item."""
|
||||
return self._title, self._widget
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldId = proxyOutboundUser.get('id', '')
|
||||
newId = self.text()
|
||||
|
||||
if isinstance(oldId, str):
|
||||
if newId != oldId:
|
||||
if newId == '':
|
||||
# Remove field
|
||||
proxyOutboundUser.pop('id', None)
|
||||
else:
|
||||
proxyOutboundUser['id'] = newId
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['id'] = newId
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('id', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVLESSItemBasicEncryption(GuiEditorItemTextInput):
|
||||
"""Represent GUI VLESS item basic encryption."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicEncryption."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldEncryption = proxyOutboundUser.get('encryption', '')
|
||||
newEncryption = self.text()
|
||||
|
||||
if isinstance(oldEncryption, str):
|
||||
if newEncryption != oldEncryption:
|
||||
if newEncryption == '':
|
||||
# Remove field
|
||||
proxyOutboundUser.pop('encryption', None)
|
||||
else:
|
||||
proxyOutboundUser['encryption'] = newEncryption
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['encryption'] = newEncryption
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('encryption', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVLESSItemBasicFlow(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI VLESS item basic flow."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicFlow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(
|
||||
[
|
||||
'',
|
||||
'none',
|
||||
'xtls-rprx-vision',
|
||||
'xtls-rprx-vision-udp443',
|
||||
]
|
||||
)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldFlow = proxyOutboundUser.get('flow', '')
|
||||
newFlow = self.text()
|
||||
|
||||
if isinstance(oldFlow, str):
|
||||
if newFlow != oldFlow:
|
||||
proxyOutboundUser['flow'] = newFlow
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['flow'] = newFlow
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('flow', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVLESSGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI VLESS group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVLESSGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def setupPageLayout(self):
|
||||
"""Set up page layout."""
|
||||
layout = QGridLayout()
|
||||
layout.setColumnStretch(1, 1)
|
||||
layout.setColumnStretch(3, 1)
|
||||
|
||||
def keepSpinBoxCompact(widget: QWidget):
|
||||
"""Handle keep spin box compact for the GUI VLESS group box basic."""
|
||||
if isinstance(widget, QSpinBox):
|
||||
widget.setSizePolicy(
|
||||
QSizePolicy.Policy.Fixed,
|
||||
widget.sizePolicy().verticalPolicy(),
|
||||
)
|
||||
|
||||
def addPair(index: int, row: int, column: int):
|
||||
"""Add pair."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
keepSpinBoxCompact(inputWidget)
|
||||
|
||||
layout.addWidget(label, row, column)
|
||||
layout.addWidget(inputWidget, row, column + 1)
|
||||
|
||||
def addFullRow(index: int, row: int):
|
||||
"""Add full row."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
keepSpinBoxCompact(inputWidget)
|
||||
|
||||
layout.addWidget(label, row, 0)
|
||||
layout.addWidget(inputWidget, row, 1, 1, 3)
|
||||
|
||||
addFullRow(0, 0)
|
||||
addFullRow(1, 1)
|
||||
addFullRow(2, 2)
|
||||
addFullRow(3, 3)
|
||||
addPair(4, 4, 0)
|
||||
addPair(5, 4, 2)
|
||||
|
||||
layout.setRowStretch(5, 1)
|
||||
|
||||
return layout
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiVLESSItemBasicAddress(title=_('Address')),
|
||||
GuiVLESSItemBasicPort(title=_('Port')),
|
||||
GuiVLESSItemBasicId(title='Id', translatable=False),
|
||||
GuiVLESSItemBasicEncryption(title=_('Encryption')),
|
||||
GuiVLESSItemBasicFlow(title=_('Flow')),
|
||||
]
|
||||
|
||||
|
||||
class GuiVLESSGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI VLESS group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVLESSGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVLESS(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI VLESS."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESS."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.VLESS.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiVLESSGroupBoxBasic(),
|
||||
GuiVLESSGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
@@ -0,0 +1,389 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI v mess."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import uuid
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiVMess']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.VMess,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.VMess),
|
||||
)
|
||||
|
||||
getProxyOutboundUser = functools.partial(
|
||||
ConfigXray.getProxyOutboundUser,
|
||||
protocol=Protocol.VMess,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.VMess),
|
||||
)
|
||||
|
||||
|
||||
class GuiVMessItemBasicAddress(GuiEditorItemTextInput):
|
||||
"""Represent GUI v mess item basic address."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicAddress."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldAddress = proxyOutboundServer.get('address', '')
|
||||
newAddress = self.text()
|
||||
|
||||
if isinstance(oldAddress, str):
|
||||
if newAddress != oldAddress:
|
||||
if newAddress == '':
|
||||
# Remove field
|
||||
proxyOutboundServer.pop('address', None)
|
||||
else:
|
||||
proxyOutboundServer['address'] = newAddress
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['address'] = newAddress
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get('address', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVMessItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI v mess item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiVMessItemBasicId(GuiEditorItemTextInput):
|
||||
"""Represent GUI v mess item basic id."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicId."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.generateButton = AppQPushButton(_('Generate'))
|
||||
self.generateButton.clicked.connect(self.generateUUID)
|
||||
|
||||
self._widget = QWidget()
|
||||
|
||||
layout = QHBoxLayout(self._widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self._input)
|
||||
layout.addWidget(self.generateButton)
|
||||
|
||||
def generateUUID(self):
|
||||
"""Handle generate uuid for the GUI v mess item basic id."""
|
||||
self.setText(str(uuid.uuid4()))
|
||||
|
||||
def widgets(self):
|
||||
"""Return the widgets owned by this editor item."""
|
||||
return self._title, self._widget
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldId = proxyOutboundUser.get('id', '')
|
||||
newId = self.text()
|
||||
|
||||
if isinstance(oldId, str):
|
||||
if newId != oldId:
|
||||
if newId == '':
|
||||
# Remove field
|
||||
proxyOutboundUser.pop('id', None)
|
||||
else:
|
||||
proxyOutboundUser['id'] = newId
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['id'] = newId
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('id', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVMessItemBasicAlterId(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI v mess item basic alter id."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicAlterId."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldAlterId = proxyOutboundUser.get('alterId')
|
||||
newAlterId = self.value()
|
||||
|
||||
if isinstance(oldAlterId, int):
|
||||
if newAlterId != oldAlterId:
|
||||
proxyOutboundUser['alterId'] = newAlterId
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['alterId'] = newAlterId
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setValue(proxyOutboundUser.get('alterId', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiVMessItemBasicSecurity(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI v mess item basic security."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicSecurity."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(
|
||||
[
|
||||
'',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'auto',
|
||||
'none',
|
||||
'zero',
|
||||
]
|
||||
)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldSecurity = proxyOutboundUser.get('security', '')
|
||||
newSecurity = self.text()
|
||||
|
||||
if isinstance(oldSecurity, str):
|
||||
if newSecurity != oldSecurity:
|
||||
proxyOutboundUser['security'] = newSecurity
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['security'] = newSecurity
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('security', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI v mess group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVMessGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def setupPageLayout(self):
|
||||
"""Set up page layout."""
|
||||
layout = QGridLayout()
|
||||
layout.setColumnStretch(4, 1)
|
||||
|
||||
def keepSpinBoxCompact(widget: QWidget):
|
||||
"""Handle keep spin box compact for the GUI v mess group box basic."""
|
||||
if isinstance(widget, QSpinBox):
|
||||
widget.setSizePolicy(
|
||||
QSizePolicy.Policy.Fixed,
|
||||
widget.sizePolicy().verticalPolicy(),
|
||||
)
|
||||
|
||||
def keepComboBoxCompact(widget: QWidget):
|
||||
"""Handle keep combo box compact for the GUI v mess group box basic."""
|
||||
if isinstance(widget, QComboBox):
|
||||
widget.setSizePolicy(
|
||||
QSizePolicy.Policy.Fixed,
|
||||
widget.sizePolicy().verticalPolicy(),
|
||||
)
|
||||
|
||||
def addPair(index: int, row: int, column: int):
|
||||
"""Add pair."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
keepSpinBoxCompact(inputWidget)
|
||||
|
||||
layout.addWidget(label, row, column)
|
||||
layout.addWidget(inputWidget, row, column + 1)
|
||||
|
||||
def addFullRow(index: int, row: int):
|
||||
"""Add full row."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
keepSpinBoxCompact(inputWidget)
|
||||
|
||||
layout.addWidget(label, row, 0)
|
||||
layout.addWidget(inputWidget, row, 1, 1, 4)
|
||||
|
||||
addFullRow(0, 0)
|
||||
addFullRow(1, 1)
|
||||
addPair(2, 2, 0)
|
||||
addPair(4, 2, 2)
|
||||
addFullRow(3, 3)
|
||||
keepComboBoxCompact(self._containers[5].widgets()[1])
|
||||
addFullRow(5, 4)
|
||||
|
||||
layout.setRowStretch(5, 1)
|
||||
|
||||
return layout
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiVMessItemBasicAddress(title=_('Address')),
|
||||
GuiVMessItemBasicPort(title=_('Port')),
|
||||
GuiVMessItemBasicId(title='Id', translatable=False),
|
||||
GuiVMessItemBasicAlterId(title='AlterId', translatable=False),
|
||||
GuiVMessItemBasicSecurity(title='Security', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVMessGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI v mess group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVMessGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVMess(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI v mess."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMess."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.VMess.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiVMessGroupBoxBasic(),
|
||||
GuiVMessGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
@@ -0,0 +1,803 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI vtls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import ConfigXray
|
||||
from Furious.Qt import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
from typing import Callable
|
||||
|
||||
__all__ = ['GuiVTLSQGroupBox']
|
||||
|
||||
STREAM_SECURITY = [
|
||||
'',
|
||||
'none',
|
||||
'tls',
|
||||
'reality',
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSItemSecurity(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI vtls item security."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemSecurity."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(STREAM_SECURITY)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
try:
|
||||
oldSecurity = streamSettings['security']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldSecurity = ''
|
||||
|
||||
newSecurity = self.text()
|
||||
|
||||
def setNewSecurity():
|
||||
"""Set new security."""
|
||||
if newSecurity == '':
|
||||
streamSettings.pop('security', None)
|
||||
else:
|
||||
streamSettings['security'] = newSecurity
|
||||
|
||||
for security in ['tls', 'reality']:
|
||||
if security == newSecurity:
|
||||
continue
|
||||
|
||||
securityKey = f'{security}Settings'
|
||||
|
||||
# Remove irrelevant settings
|
||||
streamSettings.pop(securityKey, None)
|
||||
|
||||
if isinstance(oldSecurity, str):
|
||||
if newSecurity != oldSecurity:
|
||||
setNewSecurity()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewSecurity()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
self.setText(streamSettings.get('security', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemXXXServerName(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item xxx server name."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Mandatory
|
||||
"""Initialize the GuiVTLSItemXXXServerName."""
|
||||
securityKey = kwargs.pop('securityKey')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.securityKey = securityKey
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get(self.securityKey), dict):
|
||||
streamSettings[self.securityKey] = {}
|
||||
|
||||
xxxObject = streamSettings[self.securityKey]
|
||||
|
||||
try:
|
||||
oldServerName = xxxObject.get('serverName', '')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldServerName = ''
|
||||
|
||||
newServerName = self.text()
|
||||
|
||||
def setNewServerName():
|
||||
"""Set new server name."""
|
||||
if newServerName == '':
|
||||
xxxObject.pop('serverName', None)
|
||||
else:
|
||||
xxxObject['serverName'] = newServerName
|
||||
|
||||
if isinstance(oldServerName, str):
|
||||
if newServerName != oldServerName:
|
||||
setNewServerName()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewServerName()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
xxxObject = ConfigXray.getProxyOutboundStream(config)[self.securityKey]
|
||||
|
||||
self.setText(xxxObject.get('serverName', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemTLSServerName(GuiVTLSItemXXXServerName):
|
||||
"""Represent GUI vtls item TLS server name."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemTLSServerName."""
|
||||
securityKey = kwargs.pop('securityKey', 'tlsSettings')
|
||||
|
||||
super().__init__(*args, **kwargs, securityKey=securityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityServerName(GuiVTLSItemXXXServerName):
|
||||
"""Represent GUI vtls item reality server name."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityServerName."""
|
||||
securityKey = kwargs.pop('securityKey', 'realitySettings')
|
||||
|
||||
super().__init__(*args, **kwargs, securityKey=securityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemXXXFingerprint(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item xxx fingerprint."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Mandatory
|
||||
"""Initialize the GuiVTLSItemXXXFingerprint."""
|
||||
securityKey = kwargs.pop('securityKey')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.securityKey = securityKey
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get(self.securityKey), dict):
|
||||
streamSettings[self.securityKey] = {}
|
||||
|
||||
xxxObject = streamSettings[self.securityKey]
|
||||
|
||||
try:
|
||||
oldFingerprint = xxxObject.get('fingerprint', '')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldFingerprint = ''
|
||||
|
||||
newFingerprint = self.text()
|
||||
|
||||
def setNewFingerprint():
|
||||
"""Set new fingerprint."""
|
||||
if newFingerprint == '':
|
||||
xxxObject.pop('fingerprint', None)
|
||||
else:
|
||||
xxxObject['fingerprint'] = newFingerprint
|
||||
|
||||
if isinstance(oldFingerprint, str):
|
||||
if newFingerprint != oldFingerprint:
|
||||
setNewFingerprint()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewFingerprint()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
xxxObject = ConfigXray.getProxyOutboundStream(config)[self.securityKey]
|
||||
|
||||
self.setText(xxxObject.get('fingerprint', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemTLSFingerprint(GuiVTLSItemXXXFingerprint):
|
||||
"""Represent GUI vtls item TLS fingerprint."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemTLSFingerprint."""
|
||||
securityKey = kwargs.pop('securityKey', 'tlsSettings')
|
||||
|
||||
super().__init__(*args, **kwargs, securityKey=securityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityFingerprint(GuiVTLSItemXXXFingerprint):
|
||||
"""Represent GUI vtls item reality fingerprint."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityFingerprint."""
|
||||
securityKey = kwargs.pop('securityKey', 'realitySettings')
|
||||
|
||||
super().__init__(*args, **kwargs, securityKey=securityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemTLSAlpn(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item TLS alpn."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemTLSAlpn."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get('tlsSettings'), dict):
|
||||
streamSettings['tlsSettings'] = {}
|
||||
|
||||
tlsObject = streamSettings['tlsSettings']
|
||||
|
||||
try:
|
||||
oldAlpn = ','.join(tlsObject.get('alpn', []))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldAlpn = ''
|
||||
|
||||
newAlpn = self.text()
|
||||
|
||||
def setNewAlpn():
|
||||
"""Set new alpn."""
|
||||
if newAlpn == '':
|
||||
tlsObject.pop('alpn', None)
|
||||
else:
|
||||
tlsObject['alpn'] = newAlpn.split(',')
|
||||
|
||||
if isinstance(oldAlpn, str):
|
||||
if newAlpn != oldAlpn:
|
||||
setNewAlpn()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewAlpn()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
tlsObject = ConfigXray.getProxyOutboundStream(config)['tlsSettings']
|
||||
|
||||
self.setText(','.join(tlsObject.get('alpn', [])))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemTLSXXXTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item tlsxxx text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Mandatory
|
||||
"""Initialize the GuiVTLSItemTLSXXXTextInput."""
|
||||
key = kwargs.pop('key')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get('tlsSettings'), dict):
|
||||
streamSettings['tlsSettings'] = {}
|
||||
|
||||
tlsObject = streamSettings['tlsSettings']
|
||||
|
||||
oldValue = tlsObject.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
def setNewTLSXXXValue():
|
||||
"""Set new tlsxxx value."""
|
||||
if newValue == '':
|
||||
tlsObject.pop(self.key, None)
|
||||
else:
|
||||
tlsObject[self.key] = newValue
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
setNewTLSXXXValue()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewTLSXXXValue()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
tlsObject = ConfigXray.getProxyOutboundStream(config)['tlsSettings']
|
||||
|
||||
self.setText(tlsObject.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemTLSAllowInsecure(GuiEditorItemTextCheckBox):
|
||||
"""Represent GUI vtls item TLS allow insecure."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemTLSAllowInsecure."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get('tlsSettings'), dict):
|
||||
streamSettings['tlsSettings'] = {}
|
||||
|
||||
tlsObject = streamSettings['tlsSettings']
|
||||
|
||||
try:
|
||||
oldInsecure = tlsObject.get('allowInsecure', False)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldInsecure = False
|
||||
|
||||
newInsecure = self.isChecked()
|
||||
|
||||
if newInsecure:
|
||||
if oldInsecure is not True:
|
||||
tlsObject['allowInsecure'] = True
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if oldInsecure is not False:
|
||||
tlsObject['allowInsecure'] = False
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
tlsObject = ConfigXray.getProxyOutboundStream(config)['tlsSettings']
|
||||
|
||||
self.setChecked(tlsObject.get('allowInsecure', False))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setChecked(False)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityXXX(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item reality xxx."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Mandatory
|
||||
"""Initialize the GuiVTLSItemRealityXXX."""
|
||||
realityKey = kwargs.pop('realityKey')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.realityKey = realityKey
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get('realitySettings'), dict):
|
||||
streamSettings['realitySettings'] = {}
|
||||
|
||||
realityObject = streamSettings['realitySettings']
|
||||
|
||||
try:
|
||||
oldPublicKey = realityObject.get(self.realityKey, '')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldPublicKey = ''
|
||||
|
||||
newPublicKey = self.text()
|
||||
|
||||
def setNewPublicKey():
|
||||
"""Set new public key."""
|
||||
if newPublicKey == '':
|
||||
realityObject.pop(self.realityKey, None)
|
||||
else:
|
||||
realityObject[self.realityKey] = newPublicKey
|
||||
|
||||
if isinstance(oldPublicKey, str):
|
||||
if newPublicKey != oldPublicKey:
|
||||
setNewPublicKey()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewPublicKey()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
realityObject = ConfigXray.getProxyOutboundStream(config)['realitySettings']
|
||||
|
||||
self.setText(realityObject.get(self.realityKey, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemRealityPublicKey(GuiVTLSItemRealityXXX):
|
||||
"""Represent GUI vtls item reality public key."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityPublicKey."""
|
||||
realityKey = kwargs.pop('realityKey', 'publicKey')
|
||||
|
||||
super().__init__(*args, **kwargs, realityKey=realityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityShortId(GuiVTLSItemRealityXXX):
|
||||
"""Represent GUI vtls item reality short id."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityShortId."""
|
||||
realityKey = kwargs.pop('realityKey', 'shortId')
|
||||
|
||||
super().__init__(*args, **kwargs, realityKey=realityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityMldsa65Verify(GuiVTLSItemRealityXXX):
|
||||
"""Represent GUI vtls item reality mldsa65 verify."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityMldsa65Verify."""
|
||||
realityKey = kwargs.pop('realityKey', 'mldsa65Verify')
|
||||
|
||||
super().__init__(*args, **kwargs, realityKey=realityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealitySpiderX(GuiVTLSItemRealityXXX):
|
||||
"""Represent GUI vtls item reality spider x."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealitySpiderX."""
|
||||
realityKey = kwargs.pop('realityKey', 'spiderX')
|
||||
|
||||
super().__init__(*args, **kwargs, realityKey=realityKey)
|
||||
|
||||
|
||||
class GuiVTLSPageXXX(GuiEditorWidgetQWidget):
|
||||
"""Represent GUI vtls page xxx."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageXXX."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def setSecurityText(self, text: str):
|
||||
"""Set security text."""
|
||||
security = self._containers[0]
|
||||
|
||||
if isinstance(security, GuiEditorItemTextComboBox):
|
||||
security.setText(text)
|
||||
|
||||
def connectActivated(self, func: Callable):
|
||||
"""Connect activated."""
|
||||
security = self._containers[0]
|
||||
|
||||
if isinstance(security, GuiEditorItemTextComboBox):
|
||||
security.connectActivated(func)
|
||||
|
||||
|
||||
class GuiVTLSPageEmpty(GuiVTLSPageXXX):
|
||||
"""Represent GUI vtls page empty."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageEmpty."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiVTLSItemSecurity(title='TLS', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSPageNone(GuiVTLSPageXXX):
|
||||
"""Represent GUI vtls page none."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageNone."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiVTLSItemSecurity(title='TLS', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSPageTLS(GuiVTLSPageXXX):
|
||||
"""Represent GUI vtls page TLS."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageTLS."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def setupLayout(self):
|
||||
"""Set up layout."""
|
||||
layout = QFormLayout()
|
||||
layout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
|
||||
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||
|
||||
basicLayout = QGridLayout()
|
||||
basicLayout.setColumnStretch(1, 1)
|
||||
basicLayout.setColumnStretch(3, 1)
|
||||
|
||||
def addPair(index: int, row: int, column: int):
|
||||
"""Add pair."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
basicLayout.addWidget(label, row, column)
|
||||
basicLayout.addWidget(inputWidget, row, column + 1)
|
||||
|
||||
def addBasicFullRow(index: int, row: int):
|
||||
"""Add basic full row."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
basicLayout.addWidget(label, row, 0)
|
||||
basicLayout.addWidget(inputWidget, row, 1, 1, 3)
|
||||
|
||||
def addFullRow(index: int):
|
||||
"""Add full row."""
|
||||
widgets = self._containers[index].widgets()
|
||||
|
||||
if len(widgets) == 1:
|
||||
layout.addRow(widgets[0])
|
||||
else:
|
||||
label, inputWidget = widgets
|
||||
|
||||
layout.addRow(label, inputWidget)
|
||||
|
||||
addPair(0, 0, 0)
|
||||
addPair(2, 0, 2)
|
||||
addPair(1, 1, 0)
|
||||
addPair(3, 1, 2)
|
||||
addBasicFullRow(4, 2)
|
||||
|
||||
layout.addRow(basicLayout)
|
||||
|
||||
addFullRow(5)
|
||||
addFullRow(6)
|
||||
addFullRow(7)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiVTLSItemSecurity(title='TLS', translatable=False),
|
||||
GuiVTLSItemTLSServerName(title='SNI', translatable=False),
|
||||
GuiVTLSItemTLSFingerprint(title='Fingerprint', translatable=False),
|
||||
GuiVTLSItemTLSAlpn(title='Alpn', translatable=False),
|
||||
GuiVTLSItemTLSXXXTextInput(
|
||||
title='EchConfigList',
|
||||
key='echConfigList',
|
||||
translatable=False,
|
||||
),
|
||||
GuiVTLSItemTLSXXXTextInput(
|
||||
title='VerifyPeerCertByName',
|
||||
key='verifyPeerCertByName',
|
||||
translatable=False,
|
||||
),
|
||||
GuiVTLSItemTLSXXXTextInput(
|
||||
title='PinnedPeerCertSha256',
|
||||
key='pinnedPeerCertSha256',
|
||||
translatable=False,
|
||||
),
|
||||
GuiVTLSItemTLSAllowInsecure(title='AllowInsecure', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSPageReality(GuiVTLSPageXXX):
|
||||
"""Represent GUI vtls page reality."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageReality."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def setupLayout(self):
|
||||
"""Set up layout."""
|
||||
layout = QGridLayout()
|
||||
layout.setColumnStretch(1, 1)
|
||||
layout.setColumnStretch(3, 1)
|
||||
layout.setHorizontalSpacing(12)
|
||||
|
||||
def addPair(index: int, row: int, column: int):
|
||||
"""Add pair."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
layout.addWidget(label, row, column)
|
||||
layout.addWidget(inputWidget, row, column + 1)
|
||||
|
||||
def addFullRow(index: int, row: int):
|
||||
"""Add full row."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
layout.addWidget(label, row, 0)
|
||||
layout.addWidget(inputWidget, row, 1, 1, 3)
|
||||
|
||||
addPair(0, 0, 0)
|
||||
addPair(2, 0, 2)
|
||||
addPair(1, 1, 0)
|
||||
addPair(4, 1, 2)
|
||||
addFullRow(3, 2)
|
||||
addFullRow(5, 3)
|
||||
addFullRow(6, 4)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiVTLSItemSecurity(title='TLS', translatable=False),
|
||||
GuiVTLSItemRealityServerName(title='SNI', translatable=False),
|
||||
GuiVTLSItemRealityFingerprint(title='Fingerprint', translatable=False),
|
||||
GuiVTLSItemRealityPublicKey(title='PublicKey', translatable=False),
|
||||
GuiVTLSItemRealityShortId(title='ShortId', translatable=False),
|
||||
GuiVTLSItemRealityMldsa65Verify(title='Mldsa65Verify', translatable=False),
|
||||
GuiVTLSItemRealitySpiderX(title='SpiderX', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSPageStackedWidget(QStackedWidget):
|
||||
"""Provide the GUI vtls page stacked widget."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageStackedWidget."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Corresponds to stream security
|
||||
self._pages = [
|
||||
GuiVTLSPageEmpty(),
|
||||
GuiVTLSPageNone(),
|
||||
GuiVTLSPageTLS(),
|
||||
GuiVTLSPageReality(),
|
||||
]
|
||||
|
||||
for page in self._pages:
|
||||
self.addWidget(page)
|
||||
|
||||
def page(self, index: int) -> GuiVTLSPageXXX:
|
||||
"""Return the page value."""
|
||||
return self._pages[index]
|
||||
|
||||
def connectActivated(self, func: Callable):
|
||||
"""Connect activated."""
|
||||
for page in self._pages:
|
||||
page.connectActivated(func)
|
||||
|
||||
|
||||
class GuiVTLSQGroupBox(GuiEditorItemFactory, AppQGroupBox):
|
||||
"""Group the GUI vtlsq editor controls."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVTLSQGroupBox."""
|
||||
translatable = kwargs.pop('translatable', False)
|
||||
|
||||
super().__init__('TLS', **kwargs, translatable=translatable)
|
||||
|
||||
self._config = ConfigFactory()
|
||||
|
||||
self._widget = GuiVTLSPageStackedWidget()
|
||||
self._widget.connectActivated(self.handleActivated)
|
||||
|
||||
layout = QFormLayout()
|
||||
layout.addRow(self._widget)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def currentIndex(self) -> int:
|
||||
"""Return the current index value."""
|
||||
return self._widget.currentIndex()
|
||||
|
||||
def setCurrentIndex(self, index: int):
|
||||
"""Set current index."""
|
||||
self._widget.setCurrentIndex(index)
|
||||
|
||||
def page(self, index: int) -> GuiVTLSPageXXX:
|
||||
"""Return the page value."""
|
||||
return self._widget.page(index)
|
||||
|
||||
def handleActivated(self, index: int):
|
||||
"""Handle activated."""
|
||||
page = self.page(index)
|
||||
page.factoryToInput(self._config)
|
||||
page.setSecurityText(STREAM_SECURITY[index])
|
||||
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
return self.page(self.currentIndex()).inputToFactory(config)
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
# Shallow copy
|
||||
"""Load the configuration value into the editor."""
|
||||
self._config = config
|
||||
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
security = streamSettings.get('security', '')
|
||||
|
||||
if not isinstance(security, str):
|
||||
return
|
||||
|
||||
try:
|
||||
index = STREAM_SECURITY.index(security)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
else:
|
||||
self.page(index).factoryToInput(config)
|
||||
self.setCurrentIndex(index)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,398 @@
|
||||
"""Integrate Xray configuration, editors, routing, and process startup."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Core.CoreProcessWorker import ProcessOutputRedirector
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Library.Storage import Storage
|
||||
from Furious.Plugins.API import FuriousPlugin, PluginProtocol
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
BLANK_CONFIG_XRAY,
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt.QtWidgets import showMBoxDirectRulesNotAllowed
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from .Core import XrayCore
|
||||
|
||||
__all__ = ['XrayPlugin']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _protocolId(protocol) -> str:
|
||||
"""Return a normalized protocol identifier."""
|
||||
return str(getattr(protocol, 'value', protocol)).strip().casefold()
|
||||
|
||||
|
||||
def fixLogObjectPath(config, attr: str, value: str, log=True):
|
||||
"""Resolve and normalize one log-file path in an Xray configuration."""
|
||||
try:
|
||||
path = config['log'][attr]
|
||||
except Exception:
|
||||
config['log'][attr] = path = ''
|
||||
|
||||
if not isinstance(path, (str, bytes)):
|
||||
config['log'][attr] = path = ''
|
||||
|
||||
if path == '':
|
||||
if SystemRuntime.isPythonw() and ProcessOutputRedirector.TemporaryDir.isValid():
|
||||
config['log'][attr] = ProcessOutputRedirector.TemporaryDir.filePath(value)
|
||||
else:
|
||||
config['log'][attr] = absolutePath(path)
|
||||
|
||||
result = config['log'][attr]
|
||||
if result:
|
||||
try:
|
||||
with open(result, 'x', encoding='utf-8'):
|
||||
pass
|
||||
except FileExistsError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if log:
|
||||
logger.info(
|
||||
f'{XrayCore.name()}: {attr} log is specified as \'{path}\'. '
|
||||
f'Fixed to \'{result}\''
|
||||
)
|
||||
|
||||
|
||||
def cleanRoutingRule(rule: dict):
|
||||
"""Remove empty match fields from an Xray routing rule."""
|
||||
if not isinstance(rule, dict):
|
||||
return None
|
||||
|
||||
result = {
|
||||
'type': 'field',
|
||||
'outboundTag': str(rule.get('outboundTag', 'proxy')).strip() or 'proxy',
|
||||
}
|
||||
|
||||
for key in [
|
||||
'domain',
|
||||
'ip',
|
||||
'sourceIP',
|
||||
'localIP',
|
||||
'user',
|
||||
'protocol',
|
||||
'inboundTag',
|
||||
'process',
|
||||
]:
|
||||
value = rule.get(key, [])
|
||||
if isinstance(value, list):
|
||||
value = list(str(item).strip() for item in value if str(item).strip())
|
||||
if value:
|
||||
result[key] = value
|
||||
|
||||
for key in [
|
||||
'port',
|
||||
'sourcePort',
|
||||
'localPort',
|
||||
'network',
|
||||
'vlessRoute',
|
||||
'balancerTag',
|
||||
'ruleTag',
|
||||
]:
|
||||
value = str(rule.get(key, '')).strip()
|
||||
if value:
|
||||
result[key] = value
|
||||
|
||||
return result if len(result) > 2 else None
|
||||
|
||||
|
||||
def customRoutingObjectFromSettings(routing: str):
|
||||
"""Load a named custom Xray routing object from persistent settings."""
|
||||
prefix = 'Custom:'
|
||||
if not isinstance(routing, str) or not routing.startswith(prefix):
|
||||
return None
|
||||
|
||||
routingProfile = Storage.UserRoutings().get(routing[len(prefix) :])
|
||||
if not isinstance(routingProfile, dict) or not routingProfile.get('enabled', True):
|
||||
return None
|
||||
|
||||
domainStrategy = routingProfile.get('domainStrategy', 'AsIs')
|
||||
if domainStrategy not in ['AsIs', 'IPIfNonMatch', 'IPOnDemand']:
|
||||
domainStrategy = 'AsIs'
|
||||
|
||||
rules = list(
|
||||
filter(
|
||||
lambda rule: rule is not None,
|
||||
list(cleanRoutingRule(rule) for rule in routingProfile.get('rules', [])),
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
'domainStrategy': domainStrategy,
|
||||
'domainMatcher': 'hybrid',
|
||||
'rules': rules,
|
||||
}
|
||||
|
||||
|
||||
class XrayPlugin(FuriousPlugin):
|
||||
"""Provide official Xray-core support."""
|
||||
|
||||
pluginId = 'official.xray'
|
||||
displayName = 'Xray-core'
|
||||
protocols = (
|
||||
PluginProtocol('VMess', 'VMess', 'Add VMess Server...', 10),
|
||||
PluginProtocol('VLESS', 'VLESS', 'Add VLESS Server...', 20),
|
||||
PluginProtocol('Shadowsocks', 'Shadowsocks', 'Add Shadowsocks Server...', 30),
|
||||
PluginProtocol('Trojan', 'Trojan', 'Add Trojan Server...', 40),
|
||||
PluginProtocol('SOCKS', 'SOCKS', 'Add SOCKS Server...', 70, True),
|
||||
)
|
||||
configurationTypes = (ConfigXray,)
|
||||
coreTypes = (XrayCore,)
|
||||
|
||||
def configFromString(self, config: str, **kwargs):
|
||||
"""Parse an Xray share URI when its scheme is supported."""
|
||||
if config.startswith(
|
||||
(
|
||||
'vmess://',
|
||||
'vless://',
|
||||
'ss://',
|
||||
'trojan://',
|
||||
'socks://',
|
||||
'socks5://',
|
||||
'socks5h://',
|
||||
)
|
||||
):
|
||||
return ConfigXray(config, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
def configFromDict(self, config: dict, **kwargs):
|
||||
"""Recognize Xray configuration mappings by their inbound/outbound fields."""
|
||||
if config.get('inbounds') is not None or config.get('outbounds') is not None:
|
||||
return ConfigXray(config, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
def blankConfig(self, protocol, **kwargs):
|
||||
"""Construct a blank Xray configuration for one supported protocol."""
|
||||
protocolId = _protocolId(protocol)
|
||||
factory = ConfigXray(copy.deepcopy(BLANK_CONFIG_XRAY), **kwargs)
|
||||
outbound = factory['outbounds'][0]
|
||||
|
||||
if protocolId in ('vmess', 'vless'):
|
||||
outbound['protocol'] = protocolId
|
||||
outbound['settings']['vnext'] = [
|
||||
{
|
||||
'address': '',
|
||||
'port': 0,
|
||||
'users': [{'email': PROXY_OUTBOUND_USER_EMAIL}],
|
||||
},
|
||||
]
|
||||
elif protocolId == 'socks':
|
||||
outbound['protocol'] = protocolId
|
||||
outbound['settings'] = {'address': '', 'port': 0}
|
||||
elif protocolId in ('shadowsocks', 'trojan'):
|
||||
outbound['protocol'] = protocolId
|
||||
outbound['settings']['servers'] = [
|
||||
{
|
||||
'address': '',
|
||||
'port': 0,
|
||||
'email': PROXY_OUTBOUND_USER_EMAIL,
|
||||
},
|
||||
]
|
||||
else:
|
||||
return None
|
||||
|
||||
return factory
|
||||
|
||||
def createEditorForProtocol(self, protocol, parent=None, **kwargs):
|
||||
"""Create the Xray editor matching a protocol identifier."""
|
||||
from .GuiShadowsocks import GuiShadowsocks
|
||||
from .GuiSocks import GuiSocks
|
||||
from .GuiTrojan import GuiTrojan
|
||||
from .GuiVLESS import GuiVLESS
|
||||
from .GuiVMess import GuiVMess
|
||||
|
||||
editors = {
|
||||
'vmess': GuiVMess,
|
||||
'vless': GuiVLESS,
|
||||
'shadowsocks': GuiShadowsocks,
|
||||
'socks': GuiSocks,
|
||||
'trojan': GuiTrojan,
|
||||
}
|
||||
editorType = editors.get(_protocolId(protocol))
|
||||
|
||||
return editorType(parent=parent, **kwargs) if editorType is not None else None
|
||||
|
||||
def createEditorForConfig(self, config, parent=None, **kwargs):
|
||||
"""Create the editor matching an Xray outbound protocol."""
|
||||
return self.createEditorForProtocol(config.proxyProtocol, parent, **kwargs)
|
||||
|
||||
def createRoutingEditor(self, parent=None, **kwargs):
|
||||
"""Create Xray's custom routing-profile editor."""
|
||||
from .UserRoutingWindow import UserRoutingWindow
|
||||
|
||||
return UserRoutingWindow(parent=parent, **kwargs)
|
||||
|
||||
def createMainWindowTools(self, parent=None, **kwargs):
|
||||
"""Create Xray asset-management actions for the host Tools menu."""
|
||||
from Furious.Qt import AppQAction, gettext
|
||||
|
||||
from .XrayAssetViewerWindow import XrayAssetViewerWindow
|
||||
|
||||
assetViewer = XrayAssetViewerWindow(parent=parent)
|
||||
if parent is not None:
|
||||
parent.xrayAssetViewerWindow = assetViewer
|
||||
|
||||
return (
|
||||
AppQAction(
|
||||
gettext('Manage Xray-core Asset File...'),
|
||||
callback=assetViewer.show,
|
||||
),
|
||||
)
|
||||
|
||||
def routingChoices(self):
|
||||
"""Return enabled named Xray routing profiles."""
|
||||
return tuple(
|
||||
(routing.get('remark', ''), f'Custom:{unique}')
|
||||
for unique, routing in Storage.UserRoutings().items()
|
||||
if routing.get('enabled', True)
|
||||
)
|
||||
|
||||
def configureEnvironment(self):
|
||||
"""Point Xray-core at Furious's bundled geo-asset directory."""
|
||||
os.environ['XRAY_LOCATION_ASSET'] = str(XRAY_ASSET_DIR)
|
||||
|
||||
def startCore(
|
||||
self,
|
||||
config,
|
||||
routing,
|
||||
exitCallback=None,
|
||||
msgCallback=None,
|
||||
proxyModeOnly=False,
|
||||
log=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Configure routing and start Xray-core."""
|
||||
if config.get('log') is None or not isinstance(config['log'], dict):
|
||||
config['log'] = {'access': '', 'error': '', 'loglevel': 'warning'}
|
||||
|
||||
logRedirectValue = str(uuid.uuid4())
|
||||
for attr in ['access', 'error']:
|
||||
fixLogObjectPath(config, attr, logRedirectValue, log)
|
||||
|
||||
if routing == AppBuiltinRouting.BypassMainlandChina.value:
|
||||
if not proxyModeOnly and SystemRuntime.isTUNMode():
|
||||
showMBoxDirectRulesNotAllowed()
|
||||
|
||||
return None, False
|
||||
|
||||
routingObject = {
|
||||
'domainStrategy': 'IPIfNonMatch',
|
||||
'domainMatcher': 'hybrid',
|
||||
'rules': [
|
||||
{
|
||||
'type': 'field',
|
||||
'domain': ['geosite:category-ads-all'],
|
||||
'outboundTag': 'block',
|
||||
},
|
||||
{
|
||||
'type': 'field',
|
||||
'domain': ['geosite:cn'],
|
||||
'outboundTag': 'direct',
|
||||
},
|
||||
{
|
||||
'type': 'field',
|
||||
'ip': ['geoip:private', 'geoip:cn'],
|
||||
'outboundTag': 'direct',
|
||||
},
|
||||
{
|
||||
'type': 'field',
|
||||
'port': '0-65535',
|
||||
'outboundTag': 'proxy',
|
||||
},
|
||||
],
|
||||
}
|
||||
elif routing == AppBuiltinRouting.Global.value:
|
||||
routingObject = {}
|
||||
elif customRoutingObjectFromSettings(routing) is not None:
|
||||
routingObject = customRoutingObjectFromSettings(routing)
|
||||
elif routing == AppBuiltinRouting.Custom.value:
|
||||
routingObject = config.get('routing', {})
|
||||
else:
|
||||
routingObject = {}
|
||||
|
||||
if log:
|
||||
logger.info(f'core {XrayCore.name()} configured')
|
||||
logger.info(f'routing is {routing}')
|
||||
logger.info(f'RoutingObject: {routingObject}')
|
||||
|
||||
config['routing'] = routingObject
|
||||
process = XrayCore(exitCallback=exitCallback, msgCallback=msgCallback)
|
||||
|
||||
return process, process.start(config, **kwargs)
|
||||
|
||||
def prepareDownloadTest(self, config, port: int):
|
||||
"""Create an Xray configuration with one local HTTP test inbound."""
|
||||
configcopy = config.deepcopy()
|
||||
configcopy['inbounds'] = [
|
||||
{
|
||||
'tag': 'http',
|
||||
'port': port,
|
||||
'listen': '127.0.0.1',
|
||||
'protocol': 'http',
|
||||
'sniffing': {
|
||||
'enabled': True,
|
||||
'destOverride': ['http', 'tls'],
|
||||
},
|
||||
'settings': {
|
||||
'auth': 'noauth',
|
||||
'udp': True,
|
||||
'allowTransparent': False,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
try:
|
||||
for outboundObject in configcopy['outbounds']:
|
||||
if outboundObject['tag'] == 'proxy':
|
||||
outboundObject['tag'] = f'proxy{port}'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return configcopy
|
||||
|
||||
def coreVersions(self):
|
||||
"""Return the bundled Xray-core version."""
|
||||
return (XrayCore.version(),)
|
||||
|
||||
def logTimestampPatterns(self):
|
||||
"""Return the timestamp format emitted by Xray-core."""
|
||||
return (r'\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}.\d{6}',)
|
||||
|
||||
def afterConnected(self, httpProxy=None):
|
||||
"""Update Xray geo assets after connecting when enabled."""
|
||||
if not SystemRuntime.isAssetsFolderWritable():
|
||||
logger.info(
|
||||
f'skipped auto assets update due to assets folder '
|
||||
f'\'{XRAY_ASSET_DIR}\' not writable'
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
logger.info(f'assets folder \'{XRAY_ASSET_DIR}\' is writable. Continue')
|
||||
if not AppSettings.isStateON_('AutoUpdateAssetFiles'):
|
||||
logger.info('skipped auto assets update due to settings')
|
||||
|
||||
return
|
||||
|
||||
from .XrayAssetDownloadManager import XrayAssetDownloadManager
|
||||
|
||||
try:
|
||||
assetDownloadManager = self._assetDownloadManager
|
||||
except AttributeError:
|
||||
self._assetDownloadManager = assetDownloadManager = (
|
||||
XrayAssetDownloadManager()
|
||||
)
|
||||
|
||||
assetDownloadManager.configureHttpProxy(httpProxy)
|
||||
assetDownloadManager.download()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,236 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide Qt support for Xray asset download manager."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt.WebGETManager import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
from typing import AnyStr, Union, Callable
|
||||
|
||||
import os
|
||||
import logging
|
||||
import hashlib
|
||||
import functools
|
||||
|
||||
__all__ = ['XrayAssetDownloadManager']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SHA256Worker(QtCore.QObject, QtCore.QRunnable):
|
||||
"""Run SHA-256 work in the background."""
|
||||
|
||||
finished = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, string=b''):
|
||||
# Explictly called __init__
|
||||
"""Initialize the SHA256Worker."""
|
||||
QtCore.QObject.__init__(self)
|
||||
QtCore.QRunnable.__init__(self)
|
||||
|
||||
self.string = string
|
||||
|
||||
def run(self):
|
||||
"""Run the SHA-256 worker task."""
|
||||
self.finished.emit(hashlib.sha256(self.string).hexdigest())
|
||||
|
||||
|
||||
class XrayAssetSHA256DownloadManager(WebGETManager):
|
||||
"""Coordinate Xray asset SHA-256 download operations."""
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
"""Initialize the XrayAssetSHA256DownloadManager."""
|
||||
actionMessage = kwargs.pop('actionMessage', 'download sha256')
|
||||
|
||||
super().__init__(parent, actionMessage=actionMessage)
|
||||
|
||||
@staticmethod
|
||||
def fileContent(filepath, mode='rb') -> AnyStr:
|
||||
"""Return the file content value used by the Xray asset SHA-256 download manager."""
|
||||
try:
|
||||
with open(filepath, mode) as file:
|
||||
return file.read()
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return b''
|
||||
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
"""Handle a successful network operation."""
|
||||
filepath = kwargs.pop('filepath', '')
|
||||
downloadCallback = kwargs.pop('downloadCallback', None)
|
||||
|
||||
data = networkReply.readAll().data()
|
||||
|
||||
if isinstance(data, bytes):
|
||||
datastr = data.decode('utf-8', 'replace')
|
||||
else:
|
||||
datastr = str(data)
|
||||
|
||||
logger.debug(f'data: {data}')
|
||||
|
||||
# 6068a73edfa08b63080b8d362bd4c5b069689a3548f413f43cfa58227a201571 geosite.dat
|
||||
# 3a12beaa33c81b6751b45833a0a942b9462420d91182e7fa1d768b418e604049 geoip.dat
|
||||
value = datastr.split()[0]
|
||||
|
||||
basename = os.path.basename(filepath)
|
||||
|
||||
def handleFinished(_digest, _value=''):
|
||||
"""Handle finished."""
|
||||
logger.debug(
|
||||
f'computed digest is \'{_digest}\' while repo digest is \'{_value}\''
|
||||
)
|
||||
|
||||
if _digest != _value:
|
||||
logger.info(f'digest not equal for {basename}. Start downloading asset')
|
||||
|
||||
if callable(downloadCallback):
|
||||
downloadCallback()
|
||||
else:
|
||||
logger.info(f'digest equal for {basename}. Nothing to do')
|
||||
|
||||
worker = SHA256Worker(self.fileContent(filepath))
|
||||
|
||||
worker.setAutoDelete(True)
|
||||
worker.finished.connect(functools.partial(handleFinished, _value=value))
|
||||
|
||||
AppThreadPool().start(worker)
|
||||
|
||||
def download(self, url, filepath, downloadCallback: Callable[[], None]):
|
||||
"""Download the Xray asset SHA-256 download manager."""
|
||||
self.webGET(
|
||||
url,
|
||||
filepath=str(filepath),
|
||||
downloadCallback=downloadCallback,
|
||||
)
|
||||
|
||||
|
||||
class XrayAssetAssetsDownloadManager(WebGETManager):
|
||||
"""Coordinate Xray asset assets download operations."""
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
"""Initialize the XrayAssetAssetsDownloadManager."""
|
||||
actionMessage = kwargs.pop('actionMessage', 'download assets')
|
||||
|
||||
super().__init__(parent, actionMessage=actionMessage)
|
||||
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
"""Handle a successful network operation."""
|
||||
filepath = kwargs.pop('filepath', '')
|
||||
|
||||
saveFile = QtCore.QSaveFile(filepath)
|
||||
|
||||
data = networkReply.readAll().data()
|
||||
|
||||
if not saveFile.open(QtCore.QSaveFile.OpenModeFlag.WriteOnly):
|
||||
logger.error(
|
||||
f'failed to open \'{filepath}\' for writing. {saveFile.errorString()}'
|
||||
)
|
||||
else:
|
||||
saveFile.write(data)
|
||||
|
||||
if not saveFile.commit():
|
||||
logger.error(
|
||||
f'save file to \'{filepath}\' failed. {saveFile.errorString()}'
|
||||
)
|
||||
else:
|
||||
logger.info(f'save file to \'{filepath}\' success')
|
||||
|
||||
def download(self, url, filepath):
|
||||
"""Download the Xray asset assets download manager."""
|
||||
self.webGET(url, filepath=str(filepath))
|
||||
|
||||
|
||||
class XrayAssetPairDownloadHelper:
|
||||
"""Represent Xray asset pair download helper."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the XrayAssetPairDownloadHelper."""
|
||||
sha256ActionMessage = kwargs.pop('sha256ActionMessage', 'download sha256')
|
||||
assetsActionMessage = kwargs.pop('assetsActionMessage', 'download assets')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.sha256Downloader = XrayAssetSHA256DownloadManager(
|
||||
actionMessage=sha256ActionMessage
|
||||
)
|
||||
self.assetsDownloader = XrayAssetAssetsDownloadManager(
|
||||
actionMessage=assetsActionMessage
|
||||
)
|
||||
|
||||
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
|
||||
"""Configure HTTP proxy."""
|
||||
return all(
|
||||
[
|
||||
self.sha256Downloader.configureHttpProxy(httpProxy),
|
||||
self.assetsDownloader.configureHttpProxy(httpProxy),
|
||||
]
|
||||
)
|
||||
|
||||
def download(self, sha256URL, assetsURL, filepath):
|
||||
"""Download the Xray asset pair download helper."""
|
||||
self.sha256Downloader.download(
|
||||
url=sha256URL,
|
||||
filepath=filepath,
|
||||
downloadCallback=functools.partial(
|
||||
self.assetsDownloader.download, assetsURL, filepath
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class XrayAssetDownloadManager:
|
||||
"""Coordinate Xray asset download operations."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the XrayAssetDownloadManager."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.downloadHelperGeosite = XrayAssetPairDownloadHelper(
|
||||
sha256ActionMessage='download geosite sha256',
|
||||
assetsActionMessage='download geosite assets',
|
||||
)
|
||||
self.downloadHelperGeoip = XrayAssetPairDownloadHelper(
|
||||
sha256ActionMessage='download geoip sha256',
|
||||
assetsActionMessage='download geoip assets',
|
||||
)
|
||||
|
||||
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
|
||||
"""Configure HTTP proxy."""
|
||||
return all(
|
||||
[
|
||||
self.downloadHelperGeosite.configureHttpProxy(httpProxy),
|
||||
self.downloadHelperGeoip.configureHttpProxy(httpProxy),
|
||||
]
|
||||
)
|
||||
|
||||
def download(self):
|
||||
"""Download the Xray asset download manager."""
|
||||
self.downloadHelperGeosite.download(
|
||||
sha256URL=URL_GEOSITE_SHA256,
|
||||
assetsURL=URL_GEOSITE,
|
||||
filepath=XRAY_ASSET_PATH_GEOSITE,
|
||||
)
|
||||
self.downloadHelperGeoip.download(
|
||||
sha256URL=URL_GEOIP_SHA256,
|
||||
assetsURL=URL_GEOIP,
|
||||
filepath=XRAY_ASSET_PATH_GEOIP,
|
||||
)
|
||||
@@ -0,0 +1,272 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for Xray asset viewer Qt list widget."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
import datetime
|
||||
import functools
|
||||
|
||||
__all__ = ['XrayAssetViewerQListWidget']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MBoxAssetExists(AppQMessageBox):
|
||||
"""Represent m box asset exists."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the MBoxAssetExists."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setStandardButtons(
|
||||
AppQMessageBox.StandardButton.Yes | AppQMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box asset exists."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(_(self.text()))
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
|
||||
"""Provide the Xray asset viewer Qt list widget."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the XrayAssetViewerQListWidget."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
self.setSelectionBehavior(AppQListWidget.SelectionBehavior.SelectRows)
|
||||
self.setSelectionMode(AppQListWidget.SelectionMode.ExtendedSelection)
|
||||
self.setIconSize(QtCore.QSize(64, 64))
|
||||
|
||||
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
|
||||
self.initialTheme = APP().theme()
|
||||
else:
|
||||
self.initialTheme = None
|
||||
|
||||
self.flushItem()
|
||||
|
||||
logger.info(f'Xray-core asset dir is \'{XRAY_ASSET_DIR}\'')
|
||||
|
||||
contextMenuActions = [
|
||||
AppQAction(
|
||||
_('Delete'),
|
||||
callback=lambda: self.deleteSelectedItem(),
|
||||
),
|
||||
]
|
||||
|
||||
self.contextMenu = AppQMenu(*contextMenuActions)
|
||||
|
||||
# Add actions to self in order to activate shortcuts
|
||||
self.addActions(self.contextMenu.actions())
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested)
|
||||
|
||||
@QtCore.Slot(QtCore.QPoint)
|
||||
def handleCustomContextMenuRequested(self, point):
|
||||
"""Handle custom context menu requested."""
|
||||
self.contextMenu.exec(self.viewport().mapToGlobal(point))
|
||||
|
||||
def flushItemByTheme(self, theme: str):
|
||||
"""Refresh item by theme."""
|
||||
self.clear()
|
||||
|
||||
maxlen = max(
|
||||
len(filename)
|
||||
for filename in os.listdir(XRAY_ASSET_DIR)
|
||||
if os.path.isfile(XRAY_ASSET_DIR / filename)
|
||||
)
|
||||
|
||||
for filename in os.listdir(XRAY_ASSET_DIR):
|
||||
if os.path.isfile(XRAY_ASSET_DIR / filename):
|
||||
try:
|
||||
# Exception may be raised
|
||||
epoch = os.path.getmtime(XRAY_ASSET_DIR / filename)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
mdate = ''
|
||||
else:
|
||||
mdate = datetime.datetime.fromtimestamp(epoch).strftime(
|
||||
'%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
item = QListWidgetItem(f'{filename:{maxlen + 6}}{mdate}')
|
||||
item.setFont(QFont(AppFontName()))
|
||||
|
||||
if AppSettings.isStateON_('DarkMode'):
|
||||
# Custom dark mode
|
||||
item.setIcon(bootstrapIconWhite('file-earmark.svg'))
|
||||
else:
|
||||
if theme == 'Dark':
|
||||
if PLATFORM == 'Windows':
|
||||
# Windows
|
||||
if versionToValue(PYSIDE6_VERSION) < versionToValue(
|
||||
'6.7.0'
|
||||
):
|
||||
# PySide6 < 6.7.0 has no system theme handling on Windows.
|
||||
# Always use black icon
|
||||
item.setIcon(bootstrapIcon('file-earmark.svg'))
|
||||
else:
|
||||
# PySide6 has system theme handling.
|
||||
item.setIcon(bootstrapIconWhite('file-earmark.svg'))
|
||||
else:
|
||||
item.setIcon(bootstrapIconWhite('file-earmark.svg'))
|
||||
else:
|
||||
item.setIcon(bootstrapIcon('file-earmark.svg'))
|
||||
|
||||
self.addItem(item)
|
||||
|
||||
def flushItem(self):
|
||||
"""Refresh item."""
|
||||
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
|
||||
assert self.initialTheme is not None
|
||||
|
||||
# Ubuntu 20.04. Flush by initial theme(Ubuntu 20.04 theme changes bug)
|
||||
self.flushItemByTheme(self.initialTheme)
|
||||
else:
|
||||
self.flushItemByTheme(APP().theme())
|
||||
|
||||
def appendNewItem(self, filename: str):
|
||||
"""Append new item."""
|
||||
|
||||
def append(_filename):
|
||||
"""Append the Xray asset viewer Qt list widget."""
|
||||
try:
|
||||
shutil.copy(_filename, XRAY_ASSET_DIR)
|
||||
except shutil.SameFileError:
|
||||
# Same file imported. Do nothing
|
||||
pass
|
||||
except Exception as ex:
|
||||
# Any non-exit exception
|
||||
|
||||
_mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
_mbox.setWindowTitle(_('Import'))
|
||||
_mbox.setText(_('Error import asset file'))
|
||||
_mbox.setInformativeText(str(ex))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
_mbox.open()
|
||||
else:
|
||||
self.flushItem()
|
||||
|
||||
_mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Information)
|
||||
_mbox.setWindowTitle(_('Import'))
|
||||
_mbox.setText(_('Import asset file success'))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
_mbox.open()
|
||||
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
if os.path.isfile(XRAY_ASSET_DIR / basename):
|
||||
|
||||
def handleResultCode(_filename, code):
|
||||
"""Handle result code."""
|
||||
if code == PySide6Legacy.enumValueWrapper(
|
||||
AppQMessageBox.StandardButton.Yes
|
||||
):
|
||||
append(_filename)
|
||||
else:
|
||||
# Do not overwrite
|
||||
pass
|
||||
|
||||
mbox = MBoxAssetExists(icon=AppQMessageBox.Icon.Question)
|
||||
mbox.setWindowTitle(_('Import'))
|
||||
mbox.setText(_('Asset file already exists. Overwrite?'))
|
||||
mbox.setInformativeText(basename)
|
||||
mbox.finished.connect(functools.partial(handleResultCode, filename))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
else:
|
||||
append(filename)
|
||||
|
||||
def deleteSelectedItem(self):
|
||||
"""Delete selected item."""
|
||||
indexes = self.selectedIndex
|
||||
|
||||
if len(indexes) == 0:
|
||||
# Nothing selected
|
||||
return
|
||||
|
||||
def handleResultCode(_indexes, code):
|
||||
"""Handle result code."""
|
||||
if code == PySide6Legacy.enumValueWrapper(
|
||||
AppQMessageBox.StandardButton.Yes
|
||||
):
|
||||
for index in _indexes:
|
||||
os.remove(XRAY_ASSET_DIR / self.item(index).text())
|
||||
|
||||
self.flushItem()
|
||||
else:
|
||||
# Do not delete
|
||||
pass
|
||||
|
||||
if PLATFORM == 'Windows':
|
||||
# Windows
|
||||
mbox = MBoxQuestionDelete(icon=AppQMessageBox.Icon.Question)
|
||||
else:
|
||||
# macOS & linux
|
||||
mbox = MBoxQuestionDelete(
|
||||
icon=AppQMessageBox.Icon.Question, parent=self.parent()
|
||||
)
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
mbox.isMulti = bool(len(indexes) > 1)
|
||||
mbox.possibleRemark = f'{self.item(indexes[0]).text()}'
|
||||
mbox.setText(mbox.customText())
|
||||
mbox.finished.connect(functools.partial(handleResultCode, indexes))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Handle a key press for the Xray asset viewer Qt list widget."""
|
||||
if event.key() == QtCore.Qt.Key.Key_Delete:
|
||||
self.deleteSelectedItem()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def themeChangedCallback(self, theme):
|
||||
"""Update the Xray asset viewer Qt list widget for a theme change."""
|
||||
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
|
||||
# Ubuntu 20.04 system dark theme does not
|
||||
# change menu color. Do nothing
|
||||
pass
|
||||
else:
|
||||
self.flushItemByTheme(theme)
|
||||
@@ -0,0 +1,113 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide the application window for Xray asset viewer window."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from .XrayAssetViewerQListWidget import *
|
||||
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import logging
|
||||
|
||||
__all__ = ['XrayAssetViewerWindow']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XrayAssetViewerWindow(AppQMainWindow):
|
||||
"""Present the Xray asset viewer window."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the XrayAssetViewerWindow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle(_('Xray-core Asset File'))
|
||||
|
||||
self.xrayAssetViewerWidget = XrayAssetViewerQListWidget(parent=self)
|
||||
self.setCentralWidget(self.xrayAssetViewerWidget)
|
||||
|
||||
if versionToValue(PYSIDE6_VERSION) <= versionToValue('6.1.3'):
|
||||
openAssetDirectoryActions = []
|
||||
else:
|
||||
# openUrl will crash on PySide6 6.1.3, probably a Qt bug
|
||||
openAssetDirectoryActions = [
|
||||
AppQAction(
|
||||
_('Open Asset Directory'),
|
||||
callback=lambda: self.openAssetDirectory(),
|
||||
shortcut=QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier.ControlModifier,
|
||||
QtCore.Qt.Key.Key_O,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
self.fileMenu = AppQMenu(
|
||||
AppQAction(
|
||||
_('Refresh'),
|
||||
callback=lambda: self.flushItem(),
|
||||
shortcut=QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier.ControlModifier,
|
||||
QtCore.Qt.Key.Key_R,
|
||||
),
|
||||
),
|
||||
AppQSeperator(),
|
||||
*openAssetDirectoryActions,
|
||||
AppQAction(
|
||||
_('Import From File...'),
|
||||
callback=lambda: self.appendNewItem(),
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Exit'),
|
||||
callback=lambda: self.close(),
|
||||
),
|
||||
title=_('File'),
|
||||
parent=self.menuBar(),
|
||||
)
|
||||
|
||||
self.menuBar().addMenu(self.fileMenu)
|
||||
|
||||
def setWidthAndHeight(self):
|
||||
"""Apply the default size for the Xray asset viewer window."""
|
||||
self.resize(360, 360 * GOLDEN_RATIO)
|
||||
|
||||
def flushItem(self):
|
||||
"""Refresh item."""
|
||||
self.xrayAssetViewerWidget.flushItem()
|
||||
|
||||
@staticmethod
|
||||
def openAssetDirectory():
|
||||
"""Open asset directory."""
|
||||
if QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(XRAY_ASSET_DIR)):
|
||||
logger.info(f'open Xray-core asset dir success')
|
||||
else:
|
||||
logger.error(f'open Xray-core asset dir failed')
|
||||
|
||||
def appendNewItem(self):
|
||||
"""Append new item."""
|
||||
filename, selectedFilter = QFileDialog.getOpenFileName(
|
||||
None, _('Import File'), filter=_('All files (*)')
|
||||
)
|
||||
|
||||
if filename:
|
||||
self.xrayAssetViewerWidget.appendNewItem(filename)
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Expose the official Xray plugin implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Plugin import *
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Register the core plugins maintained with Furious itself."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ['registerOfficialPlugins']
|
||||
|
||||
|
||||
def registerOfficialPlugins(registry):
|
||||
"""Register the bundled Xray and Hysteria plugins."""
|
||||
from .Xray.Plugin import XrayPlugin
|
||||
from .Hysteria1.Plugin import Hysteria1Plugin
|
||||
from .Hysteria2.Plugin import Hysteria2Plugin
|
||||
|
||||
registry.register(XrayPlugin())
|
||||
registry.register(Hysteria1Plugin())
|
||||
registry.register(Hysteria2Plugin())
|
||||
@@ -0,0 +1,350 @@
|
||||
"""Register official plugins and discover third-party Furious plugins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import metadata
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from .API import PLUGIN_API_VERSION, FuriousPlugin, PluginProtocol
|
||||
|
||||
__all__ = [
|
||||
'PLUGIN_ENTRY_POINT_GROUP',
|
||||
'PluginRegistry',
|
||||
'getPluginRegistry',
|
||||
'registerPlugin',
|
||||
]
|
||||
|
||||
PLUGIN_ENTRY_POINT_GROUP = 'furious.plugins'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _normalizeProtocol(protocol) -> str:
|
||||
"""Return a case-insensitive protocol identifier."""
|
||||
value = getattr(protocol, 'value', protocol)
|
||||
|
||||
return str(value).strip().casefold()
|
||||
|
||||
|
||||
class PluginRegistry:
|
||||
"""Store plugins and route host operations to the owning plugin."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an empty plugin registry."""
|
||||
self._plugins = {}
|
||||
self._protocols = {}
|
||||
self._configurationTypes = {}
|
||||
self._coreTypes = {}
|
||||
|
||||
def register(self, plugin: FuriousPlugin):
|
||||
"""Register a plugin after validating all of its public identifiers."""
|
||||
if isinstance(plugin, type) and issubclass(plugin, FuriousPlugin):
|
||||
plugin = plugin()
|
||||
|
||||
if not isinstance(plugin, FuriousPlugin):
|
||||
raise TypeError('plugin must be a FuriousPlugin instance')
|
||||
if plugin.apiVersion != PLUGIN_API_VERSION:
|
||||
raise ValueError(
|
||||
f'plugin API {plugin.apiVersion!r} is not supported; '
|
||||
f'expected {PLUGIN_API_VERSION}'
|
||||
)
|
||||
|
||||
pluginId = str(plugin.pluginId).strip()
|
||||
if not pluginId:
|
||||
raise ValueError('plugin ID cannot be empty')
|
||||
if pluginId in self._plugins:
|
||||
raise ValueError(f'plugin {pluginId!r} is already registered')
|
||||
|
||||
protocolKeys = []
|
||||
for descriptor in plugin.protocols:
|
||||
if not isinstance(descriptor, PluginProtocol):
|
||||
raise TypeError('plugin protocols must contain PluginProtocol values')
|
||||
|
||||
key = _normalizeProtocol(descriptor.id)
|
||||
if not key:
|
||||
raise ValueError('plugin protocol ID cannot be empty')
|
||||
if key in self._protocols or key in protocolKeys:
|
||||
raise ValueError(f'protocol {descriptor.id!r} is already registered')
|
||||
|
||||
protocolKeys.append(key)
|
||||
|
||||
configurationTypes = []
|
||||
for configType in plugin.configurationTypes:
|
||||
if not isinstance(configType, type):
|
||||
raise TypeError('plugin configuration types must be classes')
|
||||
if any(
|
||||
issubclass(configType, registeredType)
|
||||
or issubclass(registeredType, configType)
|
||||
for registeredType in (
|
||||
*self._configurationTypes,
|
||||
*configurationTypes,
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
f'configuration type {configType.__name__!r} overlaps a registered type'
|
||||
)
|
||||
configurationTypes.append(configType)
|
||||
|
||||
coreTypes = []
|
||||
for coreType in plugin.coreTypes:
|
||||
if not isinstance(coreType, type):
|
||||
raise TypeError('plugin core types must be classes')
|
||||
if any(
|
||||
issubclass(coreType, registeredType)
|
||||
or issubclass(registeredType, coreType)
|
||||
for registeredType in (
|
||||
*self._coreTypes,
|
||||
*coreTypes,
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
f'core type {coreType.__name__!r} overlaps a registered type'
|
||||
)
|
||||
coreTypes.append(coreType)
|
||||
|
||||
self._plugins[pluginId] = plugin
|
||||
|
||||
for key, descriptor in zip(protocolKeys, plugin.protocols):
|
||||
self._protocols[key] = (plugin, descriptor)
|
||||
for configType in plugin.configurationTypes:
|
||||
self._configurationTypes[configType] = plugin
|
||||
for coreType in plugin.coreTypes:
|
||||
self._coreTypes[coreType] = plugin
|
||||
|
||||
logger.info(f'registered plugin {pluginId!r}')
|
||||
|
||||
return plugin
|
||||
|
||||
def plugins(self):
|
||||
"""Return registered plugins in registration order."""
|
||||
return tuple(self._plugins.values())
|
||||
|
||||
def protocolDescriptors(self):
|
||||
"""Return contributed protocols in their requested menu order."""
|
||||
descriptors = list(
|
||||
descriptor for _plugin, descriptor in self._protocols.values()
|
||||
)
|
||||
|
||||
return tuple(sorted(descriptors, key=lambda descriptor: descriptor.menuOrder))
|
||||
|
||||
def pluginForProtocol(self, protocol):
|
||||
"""Return the plugin that owns a protocol identifier."""
|
||||
entry = self._protocols.get(_normalizeProtocol(protocol))
|
||||
|
||||
return entry[0] if entry is not None else None
|
||||
|
||||
def pluginForConfig(self, config):
|
||||
"""Return the plugin that owns a configuration instance."""
|
||||
for configType, plugin in self._configurationTypes.items():
|
||||
if isinstance(config, configType):
|
||||
return plugin
|
||||
|
||||
return None
|
||||
|
||||
def pluginForCore(self, core):
|
||||
"""Return the plugin that owns a running core instance."""
|
||||
for coreType, plugin in self._coreTypes.items():
|
||||
if isinstance(core, coreType):
|
||||
return plugin
|
||||
|
||||
return None
|
||||
|
||||
def configFromString(self, config: str, **kwargs):
|
||||
"""Ask plugins to parse textual configuration data."""
|
||||
for plugin in self.plugins():
|
||||
factory = plugin.configFromString(config, **kwargs)
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
return None
|
||||
|
||||
def configFromDict(self, config: dict, **kwargs):
|
||||
"""Ask plugins to parse configuration mapping data."""
|
||||
for plugin in self.plugins():
|
||||
factory = plugin.configFromDict(config, **kwargs)
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
return None
|
||||
|
||||
def blankConfig(self, protocol, **kwargs):
|
||||
"""Construct a blank configuration through the owning plugin."""
|
||||
plugin = self.pluginForProtocol(protocol)
|
||||
|
||||
return plugin.blankConfig(protocol, **kwargs) if plugin is not None else None
|
||||
|
||||
def createEditorForProtocol(self, protocol, parent=None, **kwargs):
|
||||
"""Construct a protocol editor through the owning plugin."""
|
||||
plugin = self.pluginForProtocol(protocol)
|
||||
if plugin is None:
|
||||
return None
|
||||
|
||||
return plugin.createEditorForProtocol(protocol, parent=parent, **kwargs)
|
||||
|
||||
def createEditorForConfig(self, config, parent=None, **kwargs):
|
||||
"""Construct a configuration editor through the owning plugin."""
|
||||
plugin = self.pluginForConfig(config)
|
||||
if plugin is None:
|
||||
return None
|
||||
|
||||
return plugin.createEditorForConfig(config, parent=parent, **kwargs)
|
||||
|
||||
def routingEditors(self, parent=None, **kwargs):
|
||||
"""Create every optional plugin-specific routing editor."""
|
||||
editors = []
|
||||
for plugin in self.plugins():
|
||||
try:
|
||||
editor = plugin.createRoutingEditor(parent=parent, **kwargs)
|
||||
if editor is not None:
|
||||
editors.append((plugin, editor))
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
f'failed to create routing editor for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
|
||||
return tuple(editors)
|
||||
|
||||
def mainWindowTools(self, parent=None, **kwargs):
|
||||
"""Return Tools-menu actions contributed by registered plugins."""
|
||||
actions = []
|
||||
for plugin in self.plugins():
|
||||
try:
|
||||
actions.extend(plugin.createMainWindowTools(parent=parent, **kwargs))
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
f'failed to create main-window tools for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
|
||||
return tuple(actions)
|
||||
|
||||
def routingChoices(self):
|
||||
"""Return unique routing choices contributed by registered plugins."""
|
||||
choices = []
|
||||
values = set()
|
||||
for plugin in self.plugins():
|
||||
try:
|
||||
for label, value in plugin.routingChoices():
|
||||
if value not in values:
|
||||
choices.append((label, value))
|
||||
values.add(value)
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
f'failed to obtain routing choices for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
|
||||
return tuple(choices)
|
||||
|
||||
def configureEnvironment(self):
|
||||
"""Allow every plugin to configure its core process environment."""
|
||||
for plugin in self.plugins():
|
||||
try:
|
||||
plugin.configureEnvironment()
|
||||
except Exception as ex:
|
||||
logger.error(f'environment hook failed for {plugin.pluginId!r}: {ex}')
|
||||
|
||||
def coreVersions(self):
|
||||
"""Return version strings reported by every registered plugin core."""
|
||||
versions = []
|
||||
for plugin in self.plugins():
|
||||
try:
|
||||
versions.extend(plugin.coreVersions())
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
f'failed to obtain core versions for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
|
||||
return tuple(filter(None, versions))
|
||||
|
||||
def logTimestampPatterns(self):
|
||||
"""Return timestamp expressions contributed by registered plugins."""
|
||||
patterns = []
|
||||
for plugin in self.plugins():
|
||||
try:
|
||||
patterns.extend(plugin.logTimestampPatterns())
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
f'failed to obtain log patterns for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
|
||||
return tuple(filter(None, patterns))
|
||||
|
||||
def coreExitMessage(self, core, exitcode: int):
|
||||
"""Return the owning plugin's special exit message, if any."""
|
||||
plugin = self.pluginForCore(core)
|
||||
if plugin is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return plugin.coreExitMessage(core, exitcode)
|
||||
except Exception as ex:
|
||||
logger.error(f'failed to interpret core exit for {plugin.pluginId!r}: {ex}')
|
||||
|
||||
return None
|
||||
|
||||
def afterConnected(self, httpProxy=None):
|
||||
"""Notify every registered plugin after a connection succeeds."""
|
||||
for plugin in self.plugins():
|
||||
try:
|
||||
plugin.afterConnected(httpProxy)
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
f'post-connection hook failed for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
|
||||
def discover(self):
|
||||
"""Load third-party plugins exposed through Python entry points."""
|
||||
try:
|
||||
entryPoints = metadata.entry_points()
|
||||
if hasattr(entryPoints, 'select'):
|
||||
entryPoints = entryPoints.select(group=PLUGIN_ENTRY_POINT_GROUP)
|
||||
else:
|
||||
entryPoints = entryPoints.get(PLUGIN_ENTRY_POINT_GROUP, tuple())
|
||||
except Exception as ex:
|
||||
logger.error(f'failed to enumerate Furious plugins: {ex}')
|
||||
|
||||
return
|
||||
|
||||
for entryPoint in entryPoints:
|
||||
try:
|
||||
plugin = entryPoint.load()
|
||||
if isinstance(plugin, type) and issubclass(plugin, FuriousPlugin):
|
||||
plugin = plugin()
|
||||
elif callable(plugin) and not isinstance(plugin, FuriousPlugin):
|
||||
plugin = plugin()
|
||||
|
||||
if isinstance(plugin, (tuple, list)):
|
||||
for item in plugin:
|
||||
self.register(item)
|
||||
else:
|
||||
self.register(plugin)
|
||||
except Exception as ex:
|
||||
logger.error(f'failed to load plugin {entryPoint.name!r}: {ex}')
|
||||
|
||||
|
||||
_registry = PluginRegistry()
|
||||
_registryLock = threading.RLock()
|
||||
_registryInitialized = False
|
||||
|
||||
|
||||
def getPluginRegistry() -> PluginRegistry:
|
||||
"""Return the process-wide registry after lazy official/external discovery."""
|
||||
global _registry, _registryInitialized
|
||||
|
||||
with _registryLock:
|
||||
if not _registryInitialized:
|
||||
from .Official import registerOfficialPlugins
|
||||
|
||||
registry = PluginRegistry()
|
||||
registerOfficialPlugins(registry)
|
||||
registry.discover()
|
||||
_registry = registry
|
||||
_registryInitialized = True
|
||||
|
||||
return _registry
|
||||
|
||||
|
||||
def registerPlugin(plugin: FuriousPlugin):
|
||||
"""Register a plugin programmatically and return it."""
|
||||
return getPluginRegistry().register(plugin)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Expose the Furious plugin API and process-wide plugin registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .API import *
|
||||
from .Configuration import *
|
||||
from .Registry import *
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Core import *
|
||||
from Furious.Core.Tun2socks import Tun2socks
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
@@ -111,9 +112,7 @@ class AppQSyntaxHighlighter(QSyntaxHighlighter):
|
||||
shouldHighlight = True
|
||||
|
||||
for version in [
|
||||
XrayCore.version(),
|
||||
Hysteria1.version(),
|
||||
Hysteria2.version(),
|
||||
*getPluginRegistry().coreVersions(),
|
||||
Tun2socks.version(),
|
||||
]:
|
||||
if captured == version:
|
||||
@@ -197,10 +196,8 @@ class DraculaLoggerSyntaxHighlighter(AppQSyntaxHighlighter):
|
||||
EditorHighlightRules(
|
||||
# Application logging timestamp
|
||||
r'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}\]' + r'|'
|
||||
# Xray-core logging timestamp
|
||||
+ r'\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}.\d{6}' + r'|'
|
||||
# hysteria2 logging timestamp
|
||||
+ r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})' + r'|'
|
||||
# Plugin-core logging timestamps
|
||||
+ r'|'.join(getPluginRegistry().logTimestampPatterns()) + r'|'
|
||||
# tun2socks logging timestamp
|
||||
+ r'\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}',
|
||||
'#7F7F7F',
|
||||
|
||||
@@ -1,236 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide Qt support for Xray asset download manager."""
|
||||
"""Provide compatibility imports for the official Xray asset updater."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt.WebGETManager import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
from typing import AnyStr, Union, Callable
|
||||
|
||||
import os
|
||||
import logging
|
||||
import hashlib
|
||||
import functools
|
||||
|
||||
__all__ = ['XrayAssetDownloadManager']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SHA256Worker(QtCore.QObject, QtCore.QRunnable):
|
||||
"""Run SHA-256 work in the background."""
|
||||
|
||||
finished = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, string=b''):
|
||||
# Explictly called __init__
|
||||
"""Initialize the SHA256Worker."""
|
||||
QtCore.QObject.__init__(self)
|
||||
QtCore.QRunnable.__init__(self)
|
||||
|
||||
self.string = string
|
||||
|
||||
def run(self):
|
||||
"""Run the SHA-256 worker task."""
|
||||
self.finished.emit(hashlib.sha256(self.string).hexdigest())
|
||||
|
||||
|
||||
class XrayAssetSHA256DownloadManager(WebGETManager):
|
||||
"""Coordinate Xray asset SHA-256 download operations."""
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
"""Initialize the XrayAssetSHA256DownloadManager."""
|
||||
actionMessage = kwargs.pop('actionMessage', 'download sha256')
|
||||
|
||||
super().__init__(parent, actionMessage=actionMessage)
|
||||
|
||||
@staticmethod
|
||||
def fileContent(filepath, mode='rb') -> AnyStr:
|
||||
"""Return the file content value used by the Xray asset SHA-256 download manager."""
|
||||
try:
|
||||
with open(filepath, mode) as file:
|
||||
return file.read()
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return b''
|
||||
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
"""Handle a successful network operation."""
|
||||
filepath = kwargs.pop('filepath', '')
|
||||
downloadCallback = kwargs.pop('downloadCallback', None)
|
||||
|
||||
data = networkReply.readAll().data()
|
||||
|
||||
if isinstance(data, bytes):
|
||||
datastr = data.decode('utf-8', 'replace')
|
||||
else:
|
||||
datastr = str(data)
|
||||
|
||||
logger.debug(f'data: {data}')
|
||||
|
||||
# 6068a73edfa08b63080b8d362bd4c5b069689a3548f413f43cfa58227a201571 geosite.dat
|
||||
# 3a12beaa33c81b6751b45833a0a942b9462420d91182e7fa1d768b418e604049 geoip.dat
|
||||
value = datastr.split()[0]
|
||||
|
||||
basename = os.path.basename(filepath)
|
||||
|
||||
def handleFinished(_digest, _value=''):
|
||||
"""Handle finished."""
|
||||
logger.debug(
|
||||
f'computed digest is \'{_digest}\' while repo digest is \'{_value}\''
|
||||
)
|
||||
|
||||
if _digest != _value:
|
||||
logger.info(f'digest not equal for {basename}. Start downloading asset')
|
||||
|
||||
if callable(downloadCallback):
|
||||
downloadCallback()
|
||||
else:
|
||||
logger.info(f'digest equal for {basename}. Nothing to do')
|
||||
|
||||
worker = SHA256Worker(self.fileContent(filepath))
|
||||
|
||||
worker.setAutoDelete(True)
|
||||
worker.finished.connect(functools.partial(handleFinished, _value=value))
|
||||
|
||||
AppThreadPool().start(worker)
|
||||
|
||||
def download(self, url, filepath, downloadCallback: Callable[[], None]):
|
||||
"""Download the Xray asset SHA-256 download manager."""
|
||||
self.webGET(
|
||||
url,
|
||||
filepath=str(filepath),
|
||||
downloadCallback=downloadCallback,
|
||||
)
|
||||
|
||||
|
||||
class XrayAssetAssetsDownloadManager(WebGETManager):
|
||||
"""Coordinate Xray asset assets download operations."""
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
"""Initialize the XrayAssetAssetsDownloadManager."""
|
||||
actionMessage = kwargs.pop('actionMessage', 'download assets')
|
||||
|
||||
super().__init__(parent, actionMessage=actionMessage)
|
||||
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
"""Handle a successful network operation."""
|
||||
filepath = kwargs.pop('filepath', '')
|
||||
|
||||
saveFile = QtCore.QSaveFile(filepath)
|
||||
|
||||
data = networkReply.readAll().data()
|
||||
|
||||
if not saveFile.open(QtCore.QSaveFile.OpenModeFlag.WriteOnly):
|
||||
logger.error(
|
||||
f'failed to open \'{filepath}\' for writing. {saveFile.errorString()}'
|
||||
)
|
||||
else:
|
||||
saveFile.write(data)
|
||||
|
||||
if not saveFile.commit():
|
||||
logger.error(
|
||||
f'save file to \'{filepath}\' failed. {saveFile.errorString()}'
|
||||
)
|
||||
else:
|
||||
logger.info(f'save file to \'{filepath}\' success')
|
||||
|
||||
def download(self, url, filepath):
|
||||
"""Download the Xray asset assets download manager."""
|
||||
self.webGET(url, filepath=str(filepath))
|
||||
|
||||
|
||||
class XrayAssetPairDownloadHelper:
|
||||
"""Represent Xray asset pair download helper."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the XrayAssetPairDownloadHelper."""
|
||||
sha256ActionMessage = kwargs.pop('sha256ActionMessage', 'download sha256')
|
||||
assetsActionMessage = kwargs.pop('assetsActionMessage', 'download assets')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.sha256Downloader = XrayAssetSHA256DownloadManager(
|
||||
actionMessage=sha256ActionMessage
|
||||
)
|
||||
self.assetsDownloader = XrayAssetAssetsDownloadManager(
|
||||
actionMessage=assetsActionMessage
|
||||
)
|
||||
|
||||
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
|
||||
"""Configure HTTP proxy."""
|
||||
return all(
|
||||
[
|
||||
self.sha256Downloader.configureHttpProxy(httpProxy),
|
||||
self.assetsDownloader.configureHttpProxy(httpProxy),
|
||||
]
|
||||
)
|
||||
|
||||
def download(self, sha256URL, assetsURL, filepath):
|
||||
"""Download the Xray asset pair download helper."""
|
||||
self.sha256Downloader.download(
|
||||
url=sha256URL,
|
||||
filepath=filepath,
|
||||
downloadCallback=functools.partial(
|
||||
self.assetsDownloader.download, assetsURL, filepath
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class XrayAssetDownloadManager:
|
||||
"""Coordinate Xray asset download operations."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the XrayAssetDownloadManager."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.downloadHelperGeosite = XrayAssetPairDownloadHelper(
|
||||
sha256ActionMessage='download geosite sha256',
|
||||
assetsActionMessage='download geosite assets',
|
||||
)
|
||||
self.downloadHelperGeoip = XrayAssetPairDownloadHelper(
|
||||
sha256ActionMessage='download geoip sha256',
|
||||
assetsActionMessage='download geoip assets',
|
||||
)
|
||||
|
||||
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
|
||||
"""Configure HTTP proxy."""
|
||||
return all(
|
||||
[
|
||||
self.downloadHelperGeosite.configureHttpProxy(httpProxy),
|
||||
self.downloadHelperGeoip.configureHttpProxy(httpProxy),
|
||||
]
|
||||
)
|
||||
|
||||
def download(self):
|
||||
"""Download the Xray asset download manager."""
|
||||
self.downloadHelperGeosite.download(
|
||||
sha256URL=URL_GEOSITE_SHA256,
|
||||
assetsURL=URL_GEOSITE,
|
||||
filepath=XRAY_ASSET_PATH_GEOSITE,
|
||||
)
|
||||
self.downloadHelperGeoip.download(
|
||||
sha256URL=URL_GEOIP_SHA256,
|
||||
assetsURL=URL_GEOIP,
|
||||
filepath=XRAY_ASSET_PATH_GEOIP,
|
||||
)
|
||||
from Furious.Plugins.Official.Xray.XrayAssetDownloadManager import *
|
||||
|
||||
@@ -32,4 +32,3 @@ from .TextEditor import *
|
||||
from .TextEditorTheme import *
|
||||
from .UpdatesManager import *
|
||||
from .WebGETManager import *
|
||||
from .XrayAssetDownloadManager import *
|
||||
|
||||
@@ -22,9 +22,10 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Core import *
|
||||
from Furious.Core.CoreManager import CoreManager
|
||||
from Furious.Widget.ConnectProgressBar import ConnectProgressBar
|
||||
|
||||
from PySide6 import QtCore
|
||||
@@ -75,7 +76,6 @@ class ConnectAction(AppQAction):
|
||||
self.actionTimer.timeout.connect(lambda: self.callActionFromQueue())
|
||||
|
||||
self.updatesManager = UpdatesManager()
|
||||
self.assetDownloadManager = XrayAssetDownloadManager()
|
||||
|
||||
def reset(self):
|
||||
"""Restore the connect action to its initial state."""
|
||||
@@ -340,19 +340,7 @@ class ConnectAction(AppQAction):
|
||||
hasNewVersionCallback=newVersionCallback,
|
||||
)
|
||||
|
||||
if not SystemRuntime.isAssetsFolderWritable():
|
||||
logger.info(
|
||||
f'skipped auto assets update due to assets folder \'{XRAY_ASSET_DIR}\' not writable'
|
||||
)
|
||||
else:
|
||||
logger.info(f'assets folder \'{XRAY_ASSET_DIR}\' is writable. Continue')
|
||||
|
||||
if AppSettings.isStateON_('AutoUpdateAssetFiles'):
|
||||
# Automatically update assets
|
||||
self.assetDownloadManager.configureHttpProxy(connectedHttpProxy)
|
||||
self.assetDownloadManager.download()
|
||||
else:
|
||||
logger.info('skipped auto assets update due to settings')
|
||||
getPluginRegistry().afterConnected(connectedHttpProxy)
|
||||
|
||||
def callActionFromQueue(self):
|
||||
"""Call action from queue."""
|
||||
@@ -406,16 +394,16 @@ class ConnectAction(AppQAction):
|
||||
|
||||
return None
|
||||
|
||||
if isinstance(core, Hysteria1):
|
||||
if exitcode == Hysteria1.ExitCode.RemoteNetworkError.value:
|
||||
putItem(
|
||||
functools.partial(
|
||||
self.doDisconnectWithTrayMessage,
|
||||
f'{core.name()}: ' + _('Connection to server has been lost'),
|
||||
)
|
||||
pluginMessage = getPluginRegistry().coreExitMessage(core, exitcode)
|
||||
if pluginMessage:
|
||||
putItem(
|
||||
functools.partial(
|
||||
self.doDisconnectWithTrayMessage,
|
||||
f'{core.name()}: ' + _(pluginMessage),
|
||||
)
|
||||
)
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
putItem(
|
||||
functools.partial(
|
||||
|
||||
@@ -21,6 +21,7 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
@@ -86,16 +87,12 @@ class RoutingAction(AppQAction):
|
||||
)
|
||||
|
||||
customActions = list()
|
||||
|
||||
for unique, routing in Storage.UserRoutings().items():
|
||||
if not routing.get('enabled', True):
|
||||
continue
|
||||
|
||||
for label, value in getPluginRegistry().routingChoices():
|
||||
action = RoutingChildAction(
|
||||
routing.get('remark', ''),
|
||||
routingValue=f'Custom:{unique}',
|
||||
label,
|
||||
routingValue=value,
|
||||
checkable=True,
|
||||
checked=AppSettings.get('Routing') == f'Custom:{unique}',
|
||||
checked=AppSettings.get('Routing') == value,
|
||||
)
|
||||
customActions.append(action)
|
||||
|
||||
|
||||
@@ -294,9 +294,10 @@ class Application(ApplicationFactory, SingletonApplication):
|
||||
|
||||
@staticmethod
|
||||
def addEnviron():
|
||||
# Xray environment variables
|
||||
"""Add environ."""
|
||||
os.environ['XRAY_LOCATION_ASSET'] = str(XRAY_ASSET_DIR)
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
|
||||
getPluginRegistry().configureEnvironment()
|
||||
|
||||
if SystemRuntime.flatpakID():
|
||||
# https://github.com/flatpak/flatpak/issues/3438
|
||||
|
||||
@@ -1,386 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI hysteria1."""
|
||||
"""Provide compatibility imports for the official Hysteria 1 editor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiHysteria1']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiHy1ItemTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy1 item text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldValue = config.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
if newValue == '':
|
||||
# Remove field
|
||||
config.pop(self.key, None)
|
||||
else:
|
||||
config[self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setText(config.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy1ItemBasicProtocol(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI hy1 item basic protocol."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemBasicProtocol."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(['', 'udp', 'wechat-video', 'faketcp'])
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldProtocol = config.get('protocol', '')
|
||||
newProtocol = self.text()
|
||||
|
||||
if isinstance(oldProtocol, str):
|
||||
if newProtocol != oldProtocol:
|
||||
config['protocol'] = newProtocol
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['protocol'] = newProtocol
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setText(config.get('protocol', 'udp'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy1ItemSpeedUpMbps(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI hy1 item speed up mbps."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemSpeedUpMbps."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 1048576)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldUpMbps = config.get('up_mbps')
|
||||
newUpMbps = self.value()
|
||||
|
||||
if isinstance(oldUpMbps, int):
|
||||
if newUpMbps != oldUpMbps:
|
||||
config['up_mbps'] = newUpMbps
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['up_mbps'] = newUpMbps
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setValue(config.get('up_mbps'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(24)
|
||||
|
||||
|
||||
class GuiHy1ItemSpeedDownMbps(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI hy1 item speed down mbps."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemSpeedDownMbps."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 1048576)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldDownMbps = config.get('down_mbps')
|
||||
newDownMbps = self.value()
|
||||
|
||||
if isinstance(oldDownMbps, int):
|
||||
if newDownMbps != oldDownMbps:
|
||||
config['down_mbps'] = newDownMbps
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['down_mbps'] = newDownMbps
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setValue(config.get('down_mbps'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(96)
|
||||
|
||||
|
||||
class GuiHy1ItemTLSInsecure(GuiEditorItemTextCheckBox):
|
||||
"""Represent GUI hy1 item TLS insecure."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ItemTLSInsecure."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
try:
|
||||
oldChecked = config.get('insecure')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldChecked = False
|
||||
|
||||
newChecked = self.isChecked()
|
||||
|
||||
if newChecked:
|
||||
if oldChecked is not True:
|
||||
config['insecure'] = True
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if oldChecked is True:
|
||||
config['insecure'] = False
|
||||
|
||||
return True
|
||||
if oldChecked is False:
|
||||
return False
|
||||
|
||||
# Invalid value for insecure. Remove it
|
||||
config.pop('insecure', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
checked = config['insecure']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
checked = False
|
||||
|
||||
if not isinstance(checked, bool):
|
||||
checked = False
|
||||
|
||||
self.setChecked(checked)
|
||||
|
||||
|
||||
class GuiHy1ProjectWebsiteURL(AppQLabel):
|
||||
"""Represent GUI hy1 project website URL."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy1ProjectWebsiteURL."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWebsiteURL()
|
||||
self.linkActivated.connect(self.handleLinkActivated)
|
||||
|
||||
def setWebsiteURL(self):
|
||||
"""Set website URL."""
|
||||
self.setText(
|
||||
'<html><head/><body><p>'
|
||||
'<a href=\"https://v1.hysteria.network/\">'
|
||||
'<span style=\" text-decoration: underline; color:#007ad6;\">'
|
||||
+ _('Project Website')
|
||||
+ '</span></a></p></body></html>'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def handleLinkActivated(link: str):
|
||||
"""Handle link activated."""
|
||||
if QDesktopServices.openUrl(QtCore.QUrl(link)):
|
||||
logger.info(f'open link \'{link}\' success')
|
||||
else:
|
||||
logger.error(f'open link \'{link}\' failed')
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the GUI hy1 project website URL."""
|
||||
self.setWebsiteURL()
|
||||
|
||||
|
||||
class GuiHy1GroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy1 group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiHy1ItemTextInput(title=_('Server'), key='server'),
|
||||
GuiHy1ItemBasicProtocol(title=_('Protocol')),
|
||||
GuiHy1ItemTextInput(title='auth_str', translatable=False, key='auth_str'),
|
||||
GuiHy1ItemTextInput(title='obfs', translatable=False, key='obfs'),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy1GroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy1 group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy1GroupBoxSpeed(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy1 group box speed."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxSpeed."""
|
||||
super().__init__(_('Speed'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy1ItemSpeedUpMbps(title='up_mbps', translatable=False),
|
||||
GuiHy1ItemSpeedDownMbps(title='down_mbps', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy1GroupBoxTLS(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy1 group box TLS."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxTLS."""
|
||||
super().__init__('TLS', **kwargs)
|
||||
|
||||
self.translatable = False
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy1ItemTextInput(title='sni', translatable=False, key='server_name'),
|
||||
GuiHy1ItemTextInput(title='alpn', translatable=False, key='alpn'),
|
||||
GuiHy1ItemTextInput(title='ca', translatable=False, key='ca'),
|
||||
GuiHy1ItemTLSInsecure(title='insecure', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy1GroupBoxOther(GuiEditorItemFactory, AppQGroupBox):
|
||||
"""Represent GUI hy1 group box other."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy1GroupBoxOther."""
|
||||
super().__init__(_('Other'), **kwargs)
|
||||
|
||||
self._website = GuiHy1ProjectWebsiteURL()
|
||||
|
||||
layout = QFormLayout()
|
||||
layout.addRow(self._website)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
pass
|
||||
|
||||
|
||||
class GuiHysteria1(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI hysteria1."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHysteria1."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Hysteria1.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiHy1GroupBoxBasic(),
|
||||
GuiHy1GroupBoxProxy(),
|
||||
GuiHy1GroupBoxSpeed(),
|
||||
GuiHy1GroupBoxTLS(),
|
||||
# GuiHy1GroupBoxOther(),
|
||||
]
|
||||
from Furious.Plugins.Official.Hysteria1.GuiHysteria1 import *
|
||||
|
||||
@@ -1,891 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI hysteria2."""
|
||||
"""Provide compatibility imports for the official Hysteria 2 editor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiHysteria2']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HY2_OBFS_TYPES = ['', 'salamander', 'gecko']
|
||||
|
||||
|
||||
class GuiHy2ItemBasicServer(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy2 item basic server."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemBasicServer."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldServer = config.get('server', '')
|
||||
newServer = self.text()
|
||||
|
||||
if isinstance(oldServer, str):
|
||||
if newServer != oldServer:
|
||||
config['server'] = newServer
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['server'] = newServer
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setText(config.get('server', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy2ItemBasicAuth(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy2 item basic auth."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemBasicAuth."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldAuth = config.get('auth')
|
||||
newAuth = self.text()
|
||||
|
||||
if newAuth == '':
|
||||
if oldAuth is None:
|
||||
return False
|
||||
else:
|
||||
config.pop('auth', None)
|
||||
|
||||
return True
|
||||
else:
|
||||
if isinstance(oldAuth, str):
|
||||
if newAuth != oldAuth:
|
||||
config['auth'] = newAuth
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
config['auth'] = newAuth
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
self.setText(config.get('auth', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy2ItemBasicCongestionComboBox(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI hy2 item basic congestion combo box."""
|
||||
|
||||
CONGESTION_KEYS = ['type', 'bbrProfile']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemBasicCongestionComboBox."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
assert key in self.CONGESTION_KEYS
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
if key == 'type':
|
||||
# TODO: Future extension
|
||||
self.addItems(['', 'bbr', 'reno'])
|
||||
elif key == 'bbrProfile':
|
||||
# TODO: Future extension
|
||||
self.addItems(['', 'standard', 'conservative', 'aggressive'])
|
||||
else:
|
||||
# Should not reach here
|
||||
raise
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
newValue = self.text()
|
||||
|
||||
if newValue == '':
|
||||
congestion = config.get('congestion')
|
||||
|
||||
if isinstance(congestion, dict):
|
||||
if len(congestion) > 0:
|
||||
if congestion.get(self.key) is not None:
|
||||
oldValue = congestion[self.key]
|
||||
|
||||
congestion.pop(self.key, None)
|
||||
|
||||
if not isinstance(oldValue, str) or oldValue != '':
|
||||
modified = True
|
||||
else:
|
||||
modified = False
|
||||
else:
|
||||
modified = False
|
||||
|
||||
# TODO: Future extension
|
||||
anyValid = any(
|
||||
congestion.get(key, '') for key in self.CONGESTION_KEYS
|
||||
)
|
||||
|
||||
if not anyValid:
|
||||
config.pop('congestion', None)
|
||||
|
||||
return True
|
||||
else:
|
||||
return modified
|
||||
else:
|
||||
config.pop('congestion', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
else:
|
||||
config.pop('congestion', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
else:
|
||||
if not isinstance(config.get('congestion'), dict):
|
||||
config['congestion'] = {}
|
||||
|
||||
oldValue = config['congestion'].get(self.key, '')
|
||||
|
||||
if not isinstance(oldValue, str) or newValue != oldValue:
|
||||
config['congestion'][self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
value = config['congestion'][self.key]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
value = ''
|
||||
|
||||
self.setText(value)
|
||||
|
||||
|
||||
class GuiHy2ItemObfsType(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI hy2 item obfs type."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemObfsType."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(HY2_OBFS_TYPES)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
try:
|
||||
oldObfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldObfsType = ''
|
||||
|
||||
newObfsType = self.text()
|
||||
|
||||
if newObfsType == '':
|
||||
if config.get('obfs') is None:
|
||||
return False
|
||||
else:
|
||||
config.pop('obfs', None)
|
||||
|
||||
return True
|
||||
else:
|
||||
if not isinstance(oldObfsType, str) or newObfsType != oldObfsType:
|
||||
if not isinstance(config.get('obfs'), dict):
|
||||
config['obfs'] = {}
|
||||
|
||||
try:
|
||||
config['obfs']['type'] = newObfsType
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if isinstance(obfsType, str) and obfsType in HY2_OBFS_TYPES:
|
||||
self.setText(obfsType)
|
||||
else:
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiHy2ItemObfsPassword(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy2 item obfs password."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemObfsPassword."""
|
||||
self.obfsType = kwargs.pop('obfsType', 'salamander')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
newObfsPassword = self.text()
|
||||
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType != self.obfsType:
|
||||
return False
|
||||
|
||||
modified = False
|
||||
|
||||
if not isinstance(config.get('obfs'), dict):
|
||||
config['obfs'] = {}
|
||||
|
||||
modified = True
|
||||
|
||||
if not isinstance(config['obfs'].get(obfsType), dict):
|
||||
config['obfs'][obfsType] = {}
|
||||
|
||||
modified = True
|
||||
|
||||
try:
|
||||
oldObfsPassword = config['obfs'][obfsType]['password']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldObfsPassword = ''
|
||||
|
||||
if not isinstance(oldObfsPassword, str) or newObfsPassword != oldObfsPassword:
|
||||
config['obfs'][obfsType]['password'] = newObfsPassword
|
||||
|
||||
return True
|
||||
else:
|
||||
return modified
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType != self.obfsType:
|
||||
self.setText('')
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
obfsPassword = config['obfs'][obfsType]['password']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsPassword = ''
|
||||
|
||||
self.setText(obfsPassword)
|
||||
|
||||
|
||||
class GuiHy2ItemObfsPacketSize(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI hy2 item obfs packet size."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemObfsPacketSize."""
|
||||
self.obfsType, self.key, self.default = (
|
||||
kwargs.pop('obfsType', 'gecko'),
|
||||
kwargs.pop('key', ''),
|
||||
kwargs.pop('default', 0),
|
||||
)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setRange(1, 2048)
|
||||
self.setValue(self.default)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType != self.obfsType:
|
||||
return False
|
||||
|
||||
if not isinstance(config.get('obfs'), dict):
|
||||
config['obfs'] = {}
|
||||
|
||||
if not isinstance(config['obfs'].get(obfsType), dict):
|
||||
config['obfs'][obfsType] = {}
|
||||
|
||||
oldValue = config['obfs'][obfsType].get(self.key)
|
||||
newValue = self.value()
|
||||
|
||||
if not isinstance(oldValue, int) or newValue != oldValue:
|
||||
config['obfs'][obfsType][self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType != self.obfsType:
|
||||
self.setValue(self.default)
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
value = config['obfs'][obfsType][self.key]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
value = self.default
|
||||
|
||||
if not isinstance(value, int):
|
||||
value = self.default
|
||||
|
||||
self.setValue(value)
|
||||
|
||||
|
||||
class GuiHy2PageObfsXXX(GuiEditorWidgetQWidget):
|
||||
"""Represent GUI hy2 page obfs xxx."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2PageObfsXXX."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def setObfsTypeText(self, text: str):
|
||||
"""Set obfs type text."""
|
||||
obfsType = self._containers[0]
|
||||
|
||||
if isinstance(obfsType, GuiEditorItemTextComboBox):
|
||||
obfsType.setText(text)
|
||||
|
||||
def connectActivated(self, func):
|
||||
"""Connect activated."""
|
||||
obfsType = self._containers[0]
|
||||
|
||||
if isinstance(obfsType, GuiEditorItemTextComboBox):
|
||||
obfsType.connectActivated(func)
|
||||
|
||||
|
||||
class GuiHy2PageObfsEmpty(GuiHy2PageObfsXXX):
|
||||
"""Represent GUI hy2 page obfs empty."""
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2PageObfsSalamander(GuiHy2PageObfsXXX):
|
||||
"""Represent GUI hy2 page obfs salamander."""
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
|
||||
GuiHy2ItemObfsPassword(
|
||||
title='obfs-password',
|
||||
obfsType='salamander',
|
||||
translatable=False,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2PageObfsGecko(GuiHy2PageObfsXXX):
|
||||
"""Represent GUI hy2 page obfs gecko."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2PageObfsGecko."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.minPacketSizeItem = self._containers[2]
|
||||
self.maxPacketSizeItem = self._containers[3]
|
||||
self.minPacketSizeItem._input.valueChanged.connect(
|
||||
self.handleMinPacketSizeChanged
|
||||
)
|
||||
|
||||
self.handleMinPacketSizeChanged(self.minPacketSizeItem.value())
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
|
||||
GuiHy2ItemObfsPassword(
|
||||
title='obfs-password',
|
||||
obfsType='gecko',
|
||||
translatable=False,
|
||||
),
|
||||
GuiHy2ItemObfsPacketSize(
|
||||
title='minPacketSize',
|
||||
key='minPacketSize',
|
||||
default=512,
|
||||
translatable=False,
|
||||
),
|
||||
GuiHy2ItemObfsPacketSize(
|
||||
title='maxPacketSize',
|
||||
key='maxPacketSize',
|
||||
default=1200,
|
||||
translatable=False,
|
||||
),
|
||||
]
|
||||
|
||||
def handleMinPacketSizeChanged(self, value: int):
|
||||
"""Handle min packet size changed."""
|
||||
self.maxPacketSizeItem._input.setMinimum(value)
|
||||
|
||||
if self.maxPacketSizeItem.value() < value:
|
||||
self.maxPacketSizeItem.setValue(value)
|
||||
|
||||
|
||||
class GuiHy2ObfsPageStackedWidget(QStackedWidget):
|
||||
"""Provide the GUI hy2 obfs page stacked widget."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ObfsPageStackedWidget."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._pages = [
|
||||
GuiHy2PageObfsEmpty(),
|
||||
GuiHy2PageObfsSalamander(),
|
||||
GuiHy2PageObfsGecko(),
|
||||
]
|
||||
|
||||
for page in self._pages:
|
||||
self.addWidget(page)
|
||||
|
||||
def page(self, index: int) -> GuiHy2PageObfsXXX:
|
||||
"""Return the page value."""
|
||||
return self._pages[index]
|
||||
|
||||
def connectActivated(self, func):
|
||||
"""Connect activated."""
|
||||
for page in self._pages:
|
||||
page.connectActivated(func)
|
||||
|
||||
|
||||
class GuiHy2ItemTLSTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI hy2 item TLS text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemTLSTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
newValue = self.text()
|
||||
|
||||
if newValue == '':
|
||||
tls = config.get('tls')
|
||||
|
||||
if isinstance(tls, dict):
|
||||
if len(tls) > 0:
|
||||
if tls.get(self.key) is not None:
|
||||
oldValue = tls[self.key]
|
||||
|
||||
tls.pop(self.key, None)
|
||||
|
||||
if not isinstance(oldValue, str) or oldValue != '':
|
||||
modified = True
|
||||
else:
|
||||
# Modified silently
|
||||
modified = False
|
||||
else:
|
||||
modified = False
|
||||
|
||||
# TODO: Future extension
|
||||
anyValid = any(
|
||||
tls.get(key, '')
|
||||
for key in ['sni', 'insecure', 'pinSHA256', 'ca']
|
||||
)
|
||||
|
||||
if not anyValid:
|
||||
config.pop('tls', None)
|
||||
|
||||
return True
|
||||
else:
|
||||
return modified
|
||||
else:
|
||||
config.pop('tls', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
else:
|
||||
config.pop('tls', None)
|
||||
|
||||
# Modified silently
|
||||
return False
|
||||
else:
|
||||
if not isinstance(config.get('tls'), dict):
|
||||
config['tls'] = {}
|
||||
|
||||
oldValue = config['tls'].get(self.key, '')
|
||||
|
||||
if not isinstance(oldValue, str) or newValue != oldValue:
|
||||
config['tls'][self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
value = config['tls'][self.key]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
value = ''
|
||||
|
||||
self.setText(value)
|
||||
|
||||
|
||||
class GuiHy2ItemTLSInsecure(GuiEditorItemTextCheckBox):
|
||||
"""Represent GUI hy2 item TLS insecure."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ItemTLSInsecure."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
try:
|
||||
oldChecked = config['tls']['insecure']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldChecked = False
|
||||
|
||||
newChecked = self.isChecked()
|
||||
|
||||
if newChecked:
|
||||
if not isinstance(config.get('tls'), dict):
|
||||
config['tls'] = {}
|
||||
|
||||
if oldChecked is not True:
|
||||
config['tls']['insecure'] = True
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if not isinstance(config.get('tls'), dict):
|
||||
config['tls'] = {}
|
||||
|
||||
if oldChecked is not False:
|
||||
config['tls']['insecure'] = False
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
checked = config['tls']['insecure']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
checked = False
|
||||
|
||||
if not isinstance(checked, bool):
|
||||
checked = False
|
||||
|
||||
self.setChecked(checked)
|
||||
|
||||
|
||||
class GuiHy2GroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy2 group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiHy2ItemBasicServer(title=_('Server')),
|
||||
GuiHy2ItemBasicAuth(title='auth', translatable=False),
|
||||
GuiHy2ItemBasicCongestionComboBox(
|
||||
title='congestion-type', key='type', translatable=False
|
||||
),
|
||||
GuiHy2ItemBasicCongestionComboBox(
|
||||
title='congestion-profile', key='bbrProfile', translatable=False
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2GroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy2 group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2GroupBoxObfs(GuiEditorItemFactory, AppQGroupBox):
|
||||
"""Represent GUI hy2 group box obfs."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxObfs."""
|
||||
translatable = kwargs.pop('translatable', False)
|
||||
|
||||
super().__init__('obfs', **kwargs, translatable=translatable)
|
||||
|
||||
self._config = ConfigFactory()
|
||||
|
||||
self._widget = GuiHy2ObfsPageStackedWidget()
|
||||
self._widget.connectActivated(self.handleActivated)
|
||||
|
||||
layout = QFormLayout()
|
||||
layout.addRow(self._widget)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def currentIndex(self) -> int:
|
||||
"""Return the current index value."""
|
||||
return self._widget.currentIndex()
|
||||
|
||||
def setCurrentIndex(self, index: int):
|
||||
"""Set current index."""
|
||||
self._widget.setCurrentIndex(index)
|
||||
|
||||
def page(self, index: int) -> GuiHy2PageObfsXXX:
|
||||
"""Return the page value."""
|
||||
return self._widget.page(index)
|
||||
|
||||
def handleActivated(self, index: int):
|
||||
"""Handle activated."""
|
||||
page = self.page(index)
|
||||
page.factoryToInput(self._config)
|
||||
page.setObfsTypeText(HY2_OBFS_TYPES[index])
|
||||
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
oldObfs = config.get('obfs')
|
||||
oldObfsType = ''
|
||||
|
||||
if isinstance(oldObfs, dict):
|
||||
oldObfsType = oldObfs.get('type', '')
|
||||
|
||||
newObfsType = HY2_OBFS_TYPES[self.currentIndex()]
|
||||
|
||||
if newObfsType == '':
|
||||
if isinstance(oldObfs, dict):
|
||||
config.pop('obfs', None)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
if newObfsType not in HY2_OBFS_TYPES:
|
||||
return False
|
||||
|
||||
if not isinstance(config.get('obfs'), dict):
|
||||
config['obfs'] = {}
|
||||
|
||||
modified = oldObfsType != newObfsType
|
||||
config['obfs']['type'] = newObfsType
|
||||
|
||||
for obfsType in HY2_OBFS_TYPES:
|
||||
if obfsType and obfsType != newObfsType:
|
||||
config['obfs'].pop(obfsType, None)
|
||||
|
||||
modified |= self.page(self.currentIndex()).inputToFactory(config)
|
||||
|
||||
return modified
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
self._config = config
|
||||
|
||||
try:
|
||||
obfsType = config['obfs']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
obfsType = ''
|
||||
|
||||
if not isinstance(obfsType, str) or obfsType not in HY2_OBFS_TYPES:
|
||||
obfsType = ''
|
||||
|
||||
index = HY2_OBFS_TYPES.index(obfsType)
|
||||
|
||||
self.page(index).factoryToInput(config)
|
||||
self.page(index).setObfsTypeText(obfsType)
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
|
||||
class GuiHy2GroupBoxTLS(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI hy2 group box TLS."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxTLS."""
|
||||
super().__init__('TLS', **kwargs)
|
||||
|
||||
self.translatable = False
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiHy2ItemTLSTextInput(title='sni', key='sni', translatable=False),
|
||||
GuiHy2ItemTLSTextInput(
|
||||
title='pinSHA256', key='pinSHA256', translatable=False
|
||||
),
|
||||
GuiHy2ItemTLSTextInput(title='ca', key='ca', translatable=False),
|
||||
GuiHy2ItemTLSInsecure(title='insecure', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiHy2ProjectWebsiteURL(AppQLabel):
|
||||
"""Represent GUI hy2 project website URL."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHy2ProjectWebsiteURL."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWebsiteURL()
|
||||
self.linkActivated.connect(self.handleLinkActivated)
|
||||
|
||||
def setWebsiteURL(self):
|
||||
"""Set website URL."""
|
||||
self.setText(
|
||||
'<html><head/><body><p>'
|
||||
'<a href=\"https://v2.hysteria.network/\">'
|
||||
'<span style=\" text-decoration: underline; color:#007ad6;\">'
|
||||
+ _('Project Website')
|
||||
+ '</span></a></p></body></html>'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def handleLinkActivated(link: str):
|
||||
"""Handle link activated."""
|
||||
if QDesktopServices.openUrl(QtCore.QUrl(link)):
|
||||
logger.info(f'open link \'{link}\' success')
|
||||
else:
|
||||
logger.error(f'open link \'{link}\' failed')
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the GUI hy2 project website URL."""
|
||||
self.setWebsiteURL()
|
||||
|
||||
|
||||
class GuiHy2GroupBoxOther(GuiEditorItemFactory, AppQGroupBox):
|
||||
"""Represent GUI hy2 group box other."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiHy2GroupBoxOther."""
|
||||
super().__init__(_('Other'), **kwargs)
|
||||
|
||||
self._website = GuiHy2ProjectWebsiteURL()
|
||||
|
||||
layout = QFormLayout()
|
||||
layout.addRow(self._website)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
pass
|
||||
|
||||
|
||||
class GuiHysteria2(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI hysteria2."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiHysteria2."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Hysteria2.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiHy2GroupBoxBasic(),
|
||||
GuiHy2GroupBoxProxy(),
|
||||
GuiHy2GroupBoxObfs(),
|
||||
GuiHy2GroupBoxTLS(),
|
||||
# GuiHy2GroupBoxOther(),
|
||||
]
|
||||
from Furious.Plugins.Official.Hysteria2.GuiHysteria2 import *
|
||||
|
||||
@@ -1,233 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI shadowsocks."""
|
||||
"""Provide compatibility imports for the official Xray Shadowsocks editor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Widget.GuiVTransport import *
|
||||
from Furious.Widget.GuiVTLS import *
|
||||
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiShadowsocks']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.Shadowsocks,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.Shadowsocks),
|
||||
)
|
||||
|
||||
|
||||
class GuiSSItemTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI ss item text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSSItemTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldValue = proxyOutboundServer.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
if newValue == '':
|
||||
# Remove field
|
||||
proxyOutboundServer.pop(self.key, None)
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiSSItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI ss item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSSItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiSSItemBasicMethod(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI ss item basic method."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSSItemBasicMethod."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(
|
||||
[
|
||||
'',
|
||||
'aes-128-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-poly1305',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
'none',
|
||||
'plain',
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
'2022-blake3-chacha20-poly1305',
|
||||
]
|
||||
)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldMethod = proxyOutboundServer.get('method', '')
|
||||
newMethod = self.text()
|
||||
|
||||
if isinstance(oldMethod, str):
|
||||
if newMethod != oldMethod:
|
||||
proxyOutboundServer['method'] = newMethod
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['method'] = newMethod
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get('method', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiSSGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI ss group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiSSGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiSSItemTextInput(title=_('Address'), key='address'),
|
||||
GuiSSItemBasicPort(title=_('Port')),
|
||||
GuiSSItemBasicMethod(title=_('Encryption')),
|
||||
GuiSSItemTextInput(title=_('Password'), key='password'),
|
||||
]
|
||||
|
||||
|
||||
class GuiSSGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI ss group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiSSGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiShadowsocks(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI shadowsocks."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiShadowsocks."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Shadowsocks.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiSSGroupBoxBasic(),
|
||||
GuiSSGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
from Furious.Plugins.Official.Xray.GuiShadowsocks import *
|
||||
|
||||
+2
-173
@@ -1,176 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI SOCKS."""
|
||||
"""Provide compatibility imports for the official Xray SOCKS editor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Widget.GuiVTransport import *
|
||||
from Furious.Widget.GuiVTLS import *
|
||||
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiSocks']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.Socks,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.Socks),
|
||||
)
|
||||
|
||||
|
||||
class GuiSocksItemTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI SOCKS item text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSocksItemTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldValue = proxyOutboundServer.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
if newValue == '':
|
||||
proxyOutboundServer.pop(self.key, None)
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiSocksItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI SOCKS item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSocksItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiSocksGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI SOCKS group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiSocksGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiSocksItemTextInput(title=_('Address'), key='address'),
|
||||
GuiSocksItemBasicPort(title=_('Port')),
|
||||
GuiSocksItemTextInput(title=_('Username'), key='user'),
|
||||
GuiSocksItemTextInput(title=_('Password'), key='pass'),
|
||||
]
|
||||
|
||||
|
||||
class GuiSocksGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI SOCKS group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiSocksGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiSocks(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI SOCKS."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiSocks."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Socks.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiSocksGroupBoxBasic(),
|
||||
GuiSocksGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
from Furious.Plugins.Official.Xray.GuiSocks import *
|
||||
|
||||
+2
-174
@@ -1,177 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI trojan."""
|
||||
"""Provide compatibility imports for the official Xray Trojan editor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Widget.GuiVTransport import *
|
||||
from Furious.Widget.GuiVTLS import *
|
||||
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiTrojan']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.Trojan,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.Trojan),
|
||||
)
|
||||
|
||||
|
||||
class GuiTrojanItemTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI trojan item text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiTrojanItemTextInput."""
|
||||
key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldValue = proxyOutboundServer.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
if newValue == '':
|
||||
# Remove field
|
||||
proxyOutboundServer.pop(self.key, None)
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer[self.key] = newValue
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiTrojanItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI trojan item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiTrojanItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiTrojanGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI trojan group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiTrojanGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiTrojanItemTextInput(title=_('Address'), key='address'),
|
||||
GuiTrojanItemBasicPort(title=_('Port')),
|
||||
GuiTrojanItemTextInput(title=_('Password'), key='password'),
|
||||
]
|
||||
|
||||
|
||||
class GuiTrojanGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI trojan group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiTrojanGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiTrojan(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI trojan."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiTrojan."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.Trojan.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiTrojanGroupBoxBasic(),
|
||||
GuiTrojanGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
from Furious.Plugins.Official.Xray.GuiTrojan import *
|
||||
|
||||
+2
-373
@@ -1,376 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI VLESS."""
|
||||
"""Provide compatibility imports for the official Xray VLESS editor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Widget.GuiVTransport import *
|
||||
from Furious.Widget.GuiVTLS import *
|
||||
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import uuid
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiVLESS']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.VLESS,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.VLESS),
|
||||
)
|
||||
|
||||
getProxyOutboundUser = functools.partial(
|
||||
ConfigXray.getProxyOutboundUser,
|
||||
protocol=Protocol.VLESS,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.VLESS),
|
||||
)
|
||||
|
||||
|
||||
class GuiVLESSItemBasicAddress(GuiEditorItemTextInput):
|
||||
"""Represent GUI VLESS item basic address."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicAddress."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldAddress = proxyOutboundServer.get('address', '')
|
||||
newAddress = self.text()
|
||||
|
||||
if isinstance(oldAddress, str):
|
||||
if newAddress != oldAddress:
|
||||
if newAddress == '':
|
||||
# Remove field
|
||||
proxyOutboundServer.pop('address', None)
|
||||
else:
|
||||
proxyOutboundServer['address'] = newAddress
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['address'] = newAddress
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get('address', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVLESSItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI VLESS item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiVLESSItemBasicId(GuiEditorItemTextInput):
|
||||
"""Represent GUI VLESS item basic id."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicId."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.generateButton = AppQPushButton(_('Generate'))
|
||||
self.generateButton.clicked.connect(self.generateUUID)
|
||||
|
||||
self._widget = QWidget()
|
||||
|
||||
layout = QHBoxLayout(self._widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self._input)
|
||||
layout.addWidget(self.generateButton)
|
||||
|
||||
def generateUUID(self):
|
||||
"""Handle generate uuid for the GUI VLESS item basic id."""
|
||||
self.setText(str(uuid.uuid4()))
|
||||
|
||||
def widgets(self):
|
||||
"""Return the widgets owned by this editor item."""
|
||||
return self._title, self._widget
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldId = proxyOutboundUser.get('id', '')
|
||||
newId = self.text()
|
||||
|
||||
if isinstance(oldId, str):
|
||||
if newId != oldId:
|
||||
if newId == '':
|
||||
# Remove field
|
||||
proxyOutboundUser.pop('id', None)
|
||||
else:
|
||||
proxyOutboundUser['id'] = newId
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['id'] = newId
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('id', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVLESSItemBasicEncryption(GuiEditorItemTextInput):
|
||||
"""Represent GUI VLESS item basic encryption."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicEncryption."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldEncryption = proxyOutboundUser.get('encryption', '')
|
||||
newEncryption = self.text()
|
||||
|
||||
if isinstance(oldEncryption, str):
|
||||
if newEncryption != oldEncryption:
|
||||
if newEncryption == '':
|
||||
# Remove field
|
||||
proxyOutboundUser.pop('encryption', None)
|
||||
else:
|
||||
proxyOutboundUser['encryption'] = newEncryption
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['encryption'] = newEncryption
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('encryption', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVLESSItemBasicFlow(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI VLESS item basic flow."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESSItemBasicFlow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(
|
||||
[
|
||||
'',
|
||||
'none',
|
||||
'xtls-rprx-vision',
|
||||
'xtls-rprx-vision-udp443',
|
||||
]
|
||||
)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldFlow = proxyOutboundUser.get('flow', '')
|
||||
newFlow = self.text()
|
||||
|
||||
if isinstance(oldFlow, str):
|
||||
if newFlow != oldFlow:
|
||||
proxyOutboundUser['flow'] = newFlow
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['flow'] = newFlow
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('flow', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVLESSGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI VLESS group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVLESSGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def setupPageLayout(self):
|
||||
"""Set up page layout."""
|
||||
layout = QGridLayout()
|
||||
layout.setColumnStretch(1, 1)
|
||||
layout.setColumnStretch(3, 1)
|
||||
|
||||
def keepSpinBoxCompact(widget: QWidget):
|
||||
"""Handle keep spin box compact for the GUI VLESS group box basic."""
|
||||
if isinstance(widget, QSpinBox):
|
||||
widget.setSizePolicy(
|
||||
QSizePolicy.Policy.Fixed,
|
||||
widget.sizePolicy().verticalPolicy(),
|
||||
)
|
||||
|
||||
def addPair(index: int, row: int, column: int):
|
||||
"""Add pair."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
keepSpinBoxCompact(inputWidget)
|
||||
|
||||
layout.addWidget(label, row, column)
|
||||
layout.addWidget(inputWidget, row, column + 1)
|
||||
|
||||
def addFullRow(index: int, row: int):
|
||||
"""Add full row."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
keepSpinBoxCompact(inputWidget)
|
||||
|
||||
layout.addWidget(label, row, 0)
|
||||
layout.addWidget(inputWidget, row, 1, 1, 3)
|
||||
|
||||
addFullRow(0, 0)
|
||||
addFullRow(1, 1)
|
||||
addFullRow(2, 2)
|
||||
addFullRow(3, 3)
|
||||
addPair(4, 4, 0)
|
||||
addPair(5, 4, 2)
|
||||
|
||||
layout.setRowStretch(5, 1)
|
||||
|
||||
return layout
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiVLESSItemBasicAddress(title=_('Address')),
|
||||
GuiVLESSItemBasicPort(title=_('Port')),
|
||||
GuiVLESSItemBasicId(title='Id', translatable=False),
|
||||
GuiVLESSItemBasicEncryption(title=_('Encryption')),
|
||||
GuiVLESSItemBasicFlow(title=_('Flow')),
|
||||
]
|
||||
|
||||
|
||||
class GuiVLESSGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI VLESS group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVLESSGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVLESS(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI VLESS."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVLESS."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.VLESS.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiVLESSGroupBoxBasic(),
|
||||
GuiVLESSGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
from Furious.Plugins.Official.Xray.GuiVLESS import *
|
||||
|
||||
+2
-382
@@ -1,385 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI v mess."""
|
||||
"""Provide compatibility imports for the official Xray VMess editor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Widget.GuiVTransport import *
|
||||
from Furious.Widget.GuiVTLS import *
|
||||
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import uuid
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiVMess']
|
||||
|
||||
getProxyOutboundServer = functools.partial(
|
||||
ConfigXray.getProxyOutboundServer,
|
||||
protocol=Protocol.VMess,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.VMess),
|
||||
)
|
||||
|
||||
getProxyOutboundUser = functools.partial(
|
||||
ConfigXray.getProxyOutboundUser,
|
||||
protocol=Protocol.VMess,
|
||||
default=configXrayEmptyProxyOutboundObject(Protocol.VMess),
|
||||
)
|
||||
|
||||
|
||||
class GuiVMessItemBasicAddress(GuiEditorItemTextInput):
|
||||
"""Represent GUI v mess item basic address."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicAddress."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldAddress = proxyOutboundServer.get('address', '')
|
||||
newAddress = self.text()
|
||||
|
||||
if isinstance(oldAddress, str):
|
||||
if newAddress != oldAddress:
|
||||
if newAddress == '':
|
||||
# Remove field
|
||||
proxyOutboundServer.pop('address', None)
|
||||
else:
|
||||
proxyOutboundServer['address'] = newAddress
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['address'] = newAddress
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setText(proxyOutboundServer.get('address', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVMessItemBasicPort(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI v mess item basic port."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicPort."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
oldPort = proxyOutboundServer.get('port')
|
||||
newPort = self.value()
|
||||
|
||||
if isinstance(oldPort, int):
|
||||
if newPort != oldPort:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundServer['port'] = newPort
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundServer = getProxyOutboundServer(config)
|
||||
|
||||
self.setValue(proxyOutboundServer.get('port', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiVMessItemBasicId(GuiEditorItemTextInput):
|
||||
"""Represent GUI v mess item basic id."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicId."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.generateButton = AppQPushButton(_('Generate'))
|
||||
self.generateButton.clicked.connect(self.generateUUID)
|
||||
|
||||
self._widget = QWidget()
|
||||
|
||||
layout = QHBoxLayout(self._widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self._input)
|
||||
layout.addWidget(self.generateButton)
|
||||
|
||||
def generateUUID(self):
|
||||
"""Handle generate uuid for the GUI v mess item basic id."""
|
||||
self.setText(str(uuid.uuid4()))
|
||||
|
||||
def widgets(self):
|
||||
"""Return the widgets owned by this editor item."""
|
||||
return self._title, self._widget
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldId = proxyOutboundUser.get('id', '')
|
||||
newId = self.text()
|
||||
|
||||
if isinstance(oldId, str):
|
||||
if newId != oldId:
|
||||
if newId == '':
|
||||
# Remove field
|
||||
proxyOutboundUser.pop('id', None)
|
||||
else:
|
||||
proxyOutboundUser['id'] = newId
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['id'] = newId
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('id', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVMessItemBasicAlterId(GuiEditorItemTextSpinBox):
|
||||
"""Represent GUI v mess item basic alter id."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicAlterId."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Range
|
||||
self.setRange(0, 65535)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldAlterId = proxyOutboundUser.get('alterId')
|
||||
newAlterId = self.value()
|
||||
|
||||
if isinstance(oldAlterId, int):
|
||||
if newAlterId != oldAlterId:
|
||||
proxyOutboundUser['alterId'] = newAlterId
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['alterId'] = newAlterId
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setValue(proxyOutboundUser.get('alterId', 0))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
class GuiVMessItemBasicSecurity(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI v mess item basic security."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMessItemBasicSecurity."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(
|
||||
[
|
||||
'',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'auto',
|
||||
'none',
|
||||
'zero',
|
||||
]
|
||||
)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
oldSecurity = proxyOutboundUser.get('security', '')
|
||||
newSecurity = self.text()
|
||||
|
||||
if isinstance(oldSecurity, str):
|
||||
if newSecurity != oldSecurity:
|
||||
proxyOutboundUser['security'] = newSecurity
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
proxyOutboundUser['security'] = newSecurity
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
proxyOutboundUser = getProxyOutboundUser(config)
|
||||
|
||||
self.setText(proxyOutboundUser.get('security', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI v mess group box basic."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVMessGroupBoxBasic."""
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def setupPageLayout(self):
|
||||
"""Set up page layout."""
|
||||
layout = QGridLayout()
|
||||
layout.setColumnStretch(4, 1)
|
||||
|
||||
def keepSpinBoxCompact(widget: QWidget):
|
||||
"""Handle keep spin box compact for the GUI v mess group box basic."""
|
||||
if isinstance(widget, QSpinBox):
|
||||
widget.setSizePolicy(
|
||||
QSizePolicy.Policy.Fixed,
|
||||
widget.sizePolicy().verticalPolicy(),
|
||||
)
|
||||
|
||||
def keepComboBoxCompact(widget: QWidget):
|
||||
"""Handle keep combo box compact for the GUI v mess group box basic."""
|
||||
if isinstance(widget, QComboBox):
|
||||
widget.setSizePolicy(
|
||||
QSizePolicy.Policy.Fixed,
|
||||
widget.sizePolicy().verticalPolicy(),
|
||||
)
|
||||
|
||||
def addPair(index: int, row: int, column: int):
|
||||
"""Add pair."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
keepSpinBoxCompact(inputWidget)
|
||||
|
||||
layout.addWidget(label, row, column)
|
||||
layout.addWidget(inputWidget, row, column + 1)
|
||||
|
||||
def addFullRow(index: int, row: int):
|
||||
"""Add full row."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
keepSpinBoxCompact(inputWidget)
|
||||
|
||||
layout.addWidget(label, row, 0)
|
||||
layout.addWidget(inputWidget, row, 1, 1, 4)
|
||||
|
||||
addFullRow(0, 0)
|
||||
addFullRow(1, 1)
|
||||
addPair(2, 2, 0)
|
||||
addPair(4, 2, 2)
|
||||
addFullRow(3, 3)
|
||||
keepComboBoxCompact(self._containers[5].widgets()[1])
|
||||
addFullRow(5, 4)
|
||||
|
||||
layout.setRowStretch(5, 1)
|
||||
|
||||
return layout
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
GuiVMessItemBasicAddress(title=_('Address')),
|
||||
GuiVMessItemBasicPort(title=_('Port')),
|
||||
GuiVMessItemBasicId(title='Id', translatable=False),
|
||||
GuiVMessItemBasicAlterId(title='AlterId', translatable=False),
|
||||
GuiVMessItemBasicSecurity(title='Security', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVMessGroupBoxProxy(GuiEditorWidgetQGroupBox):
|
||||
"""Represent GUI v mess group box proxy."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVMessGroupBoxProxy."""
|
||||
super().__init__(_('Proxy'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiEditorItemProxyHttp(title='http', translatable=False),
|
||||
GuiEditorItemProxySocks(title='socks', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVMess(GuiEditorWidgetQDialog):
|
||||
"""Represent GUI v mess."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVMess."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(Protocol.VMess.value)
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def groupBoxSequence(self):
|
||||
"""Return the configuration group boxes in display order."""
|
||||
return [
|
||||
GuiVMessGroupBoxBasic(),
|
||||
GuiVMessGroupBoxProxy(),
|
||||
GuiVTransportQGroupBox(),
|
||||
GuiVTLSQGroupBox(),
|
||||
]
|
||||
from Furious.Plugins.Official.Xray.GuiVMess import *
|
||||
|
||||
+2
-799
@@ -1,802 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for GUI vtls."""
|
||||
"""Provide compatibility imports for the official Xray TLS editor components."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Qt import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
from typing import Callable
|
||||
|
||||
__all__ = ['GuiVTLSQGroupBox']
|
||||
|
||||
STREAM_SECURITY = [
|
||||
'',
|
||||
'none',
|
||||
'tls',
|
||||
'reality',
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSItemSecurity(GuiEditorItemTextComboBox):
|
||||
"""Represent GUI vtls item security."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemSecurity."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.addItems(STREAM_SECURITY)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
try:
|
||||
oldSecurity = streamSettings['security']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldSecurity = ''
|
||||
|
||||
newSecurity = self.text()
|
||||
|
||||
def setNewSecurity():
|
||||
"""Set new security."""
|
||||
if newSecurity == '':
|
||||
streamSettings.pop('security', None)
|
||||
else:
|
||||
streamSettings['security'] = newSecurity
|
||||
|
||||
for security in ['tls', 'reality']:
|
||||
if security == newSecurity:
|
||||
continue
|
||||
|
||||
securityKey = f'{security}Settings'
|
||||
|
||||
# Remove irrelevant settings
|
||||
streamSettings.pop(securityKey, None)
|
||||
|
||||
if isinstance(oldSecurity, str):
|
||||
if newSecurity != oldSecurity:
|
||||
setNewSecurity()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewSecurity()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
self.setText(streamSettings.get('security', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemXXXServerName(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item xxx server name."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Mandatory
|
||||
"""Initialize the GuiVTLSItemXXXServerName."""
|
||||
securityKey = kwargs.pop('securityKey')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.securityKey = securityKey
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get(self.securityKey), dict):
|
||||
streamSettings[self.securityKey] = {}
|
||||
|
||||
xxxObject = streamSettings[self.securityKey]
|
||||
|
||||
try:
|
||||
oldServerName = xxxObject.get('serverName', '')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldServerName = ''
|
||||
|
||||
newServerName = self.text()
|
||||
|
||||
def setNewServerName():
|
||||
"""Set new server name."""
|
||||
if newServerName == '':
|
||||
xxxObject.pop('serverName', None)
|
||||
else:
|
||||
xxxObject['serverName'] = newServerName
|
||||
|
||||
if isinstance(oldServerName, str):
|
||||
if newServerName != oldServerName:
|
||||
setNewServerName()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewServerName()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
xxxObject = ConfigXray.getProxyOutboundStream(config)[self.securityKey]
|
||||
|
||||
self.setText(xxxObject.get('serverName', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemTLSServerName(GuiVTLSItemXXXServerName):
|
||||
"""Represent GUI vtls item TLS server name."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemTLSServerName."""
|
||||
securityKey = kwargs.pop('securityKey', 'tlsSettings')
|
||||
|
||||
super().__init__(*args, **kwargs, securityKey=securityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityServerName(GuiVTLSItemXXXServerName):
|
||||
"""Represent GUI vtls item reality server name."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityServerName."""
|
||||
securityKey = kwargs.pop('securityKey', 'realitySettings')
|
||||
|
||||
super().__init__(*args, **kwargs, securityKey=securityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemXXXFingerprint(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item xxx fingerprint."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Mandatory
|
||||
"""Initialize the GuiVTLSItemXXXFingerprint."""
|
||||
securityKey = kwargs.pop('securityKey')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.securityKey = securityKey
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get(self.securityKey), dict):
|
||||
streamSettings[self.securityKey] = {}
|
||||
|
||||
xxxObject = streamSettings[self.securityKey]
|
||||
|
||||
try:
|
||||
oldFingerprint = xxxObject.get('fingerprint', '')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldFingerprint = ''
|
||||
|
||||
newFingerprint = self.text()
|
||||
|
||||
def setNewFingerprint():
|
||||
"""Set new fingerprint."""
|
||||
if newFingerprint == '':
|
||||
xxxObject.pop('fingerprint', None)
|
||||
else:
|
||||
xxxObject['fingerprint'] = newFingerprint
|
||||
|
||||
if isinstance(oldFingerprint, str):
|
||||
if newFingerprint != oldFingerprint:
|
||||
setNewFingerprint()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewFingerprint()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
xxxObject = ConfigXray.getProxyOutboundStream(config)[self.securityKey]
|
||||
|
||||
self.setText(xxxObject.get('fingerprint', ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemTLSFingerprint(GuiVTLSItemXXXFingerprint):
|
||||
"""Represent GUI vtls item TLS fingerprint."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemTLSFingerprint."""
|
||||
securityKey = kwargs.pop('securityKey', 'tlsSettings')
|
||||
|
||||
super().__init__(*args, **kwargs, securityKey=securityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityFingerprint(GuiVTLSItemXXXFingerprint):
|
||||
"""Represent GUI vtls item reality fingerprint."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityFingerprint."""
|
||||
securityKey = kwargs.pop('securityKey', 'realitySettings')
|
||||
|
||||
super().__init__(*args, **kwargs, securityKey=securityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemTLSAlpn(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item TLS alpn."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemTLSAlpn."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get('tlsSettings'), dict):
|
||||
streamSettings['tlsSettings'] = {}
|
||||
|
||||
tlsObject = streamSettings['tlsSettings']
|
||||
|
||||
try:
|
||||
oldAlpn = ','.join(tlsObject.get('alpn', []))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldAlpn = ''
|
||||
|
||||
newAlpn = self.text()
|
||||
|
||||
def setNewAlpn():
|
||||
"""Set new alpn."""
|
||||
if newAlpn == '':
|
||||
tlsObject.pop('alpn', None)
|
||||
else:
|
||||
tlsObject['alpn'] = newAlpn.split(',')
|
||||
|
||||
if isinstance(oldAlpn, str):
|
||||
if newAlpn != oldAlpn:
|
||||
setNewAlpn()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewAlpn()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
tlsObject = ConfigXray.getProxyOutboundStream(config)['tlsSettings']
|
||||
|
||||
self.setText(','.join(tlsObject.get('alpn', [])))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemTLSXXXTextInput(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item tlsxxx text input."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Mandatory
|
||||
"""Initialize the GuiVTLSItemTLSXXXTextInput."""
|
||||
key = kwargs.pop('key')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.key = key
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get('tlsSettings'), dict):
|
||||
streamSettings['tlsSettings'] = {}
|
||||
|
||||
tlsObject = streamSettings['tlsSettings']
|
||||
|
||||
oldValue = tlsObject.get(self.key, '')
|
||||
newValue = self.text()
|
||||
|
||||
def setNewTLSXXXValue():
|
||||
"""Set new tlsxxx value."""
|
||||
if newValue == '':
|
||||
tlsObject.pop(self.key, None)
|
||||
else:
|
||||
tlsObject[self.key] = newValue
|
||||
|
||||
if isinstance(oldValue, str):
|
||||
if newValue != oldValue:
|
||||
setNewTLSXXXValue()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewTLSXXXValue()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
tlsObject = ConfigXray.getProxyOutboundStream(config)['tlsSettings']
|
||||
|
||||
self.setText(tlsObject.get(self.key, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemTLSAllowInsecure(GuiEditorItemTextCheckBox):
|
||||
"""Represent GUI vtls item TLS allow insecure."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemTLSAllowInsecure."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get('tlsSettings'), dict):
|
||||
streamSettings['tlsSettings'] = {}
|
||||
|
||||
tlsObject = streamSettings['tlsSettings']
|
||||
|
||||
try:
|
||||
oldInsecure = tlsObject.get('allowInsecure', False)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldInsecure = False
|
||||
|
||||
newInsecure = self.isChecked()
|
||||
|
||||
if newInsecure:
|
||||
if oldInsecure is not True:
|
||||
tlsObject['allowInsecure'] = True
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if oldInsecure is not False:
|
||||
tlsObject['allowInsecure'] = False
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
tlsObject = ConfigXray.getProxyOutboundStream(config)['tlsSettings']
|
||||
|
||||
self.setChecked(tlsObject.get('allowInsecure', False))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setChecked(False)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityXXX(GuiEditorItemTextInput):
|
||||
"""Represent GUI vtls item reality xxx."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Mandatory
|
||||
"""Initialize the GuiVTLSItemRealityXXX."""
|
||||
realityKey = kwargs.pop('realityKey')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.realityKey = realityKey
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
|
||||
if not isinstance(streamSettings.get('realitySettings'), dict):
|
||||
streamSettings['realitySettings'] = {}
|
||||
|
||||
realityObject = streamSettings['realitySettings']
|
||||
|
||||
try:
|
||||
oldPublicKey = realityObject.get(self.realityKey, '')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
oldPublicKey = ''
|
||||
|
||||
newPublicKey = self.text()
|
||||
|
||||
def setNewPublicKey():
|
||||
"""Set new public key."""
|
||||
if newPublicKey == '':
|
||||
realityObject.pop(self.realityKey, None)
|
||||
else:
|
||||
realityObject[self.realityKey] = newPublicKey
|
||||
|
||||
if isinstance(oldPublicKey, str):
|
||||
if newPublicKey != oldPublicKey:
|
||||
setNewPublicKey()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
setNewPublicKey()
|
||||
|
||||
return True
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
"""Load the configuration value into the editor."""
|
||||
try:
|
||||
realityObject = ConfigXray.getProxyOutboundStream(config)['realitySettings']
|
||||
|
||||
self.setText(realityObject.get(self.realityKey, ''))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setText('')
|
||||
|
||||
|
||||
class GuiVTLSItemRealityPublicKey(GuiVTLSItemRealityXXX):
|
||||
"""Represent GUI vtls item reality public key."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityPublicKey."""
|
||||
realityKey = kwargs.pop('realityKey', 'publicKey')
|
||||
|
||||
super().__init__(*args, **kwargs, realityKey=realityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityShortId(GuiVTLSItemRealityXXX):
|
||||
"""Represent GUI vtls item reality short id."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityShortId."""
|
||||
realityKey = kwargs.pop('realityKey', 'shortId')
|
||||
|
||||
super().__init__(*args, **kwargs, realityKey=realityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealityMldsa65Verify(GuiVTLSItemRealityXXX):
|
||||
"""Represent GUI vtls item reality mldsa65 verify."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealityMldsa65Verify."""
|
||||
realityKey = kwargs.pop('realityKey', 'mldsa65Verify')
|
||||
|
||||
super().__init__(*args, **kwargs, realityKey=realityKey)
|
||||
|
||||
|
||||
class GuiVTLSItemRealitySpiderX(GuiVTLSItemRealityXXX):
|
||||
"""Represent GUI vtls item reality spider x."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSItemRealitySpiderX."""
|
||||
realityKey = kwargs.pop('realityKey', 'spiderX')
|
||||
|
||||
super().__init__(*args, **kwargs, realityKey=realityKey)
|
||||
|
||||
|
||||
class GuiVTLSPageXXX(GuiEditorWidgetQWidget):
|
||||
"""Represent GUI vtls page xxx."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageXXX."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def setSecurityText(self, text: str):
|
||||
"""Set security text."""
|
||||
security = self._containers[0]
|
||||
|
||||
if isinstance(security, GuiEditorItemTextComboBox):
|
||||
security.setText(text)
|
||||
|
||||
def connectActivated(self, func: Callable):
|
||||
"""Connect activated."""
|
||||
security = self._containers[0]
|
||||
|
||||
if isinstance(security, GuiEditorItemTextComboBox):
|
||||
security.connectActivated(func)
|
||||
|
||||
|
||||
class GuiVTLSPageEmpty(GuiVTLSPageXXX):
|
||||
"""Represent GUI vtls page empty."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageEmpty."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiVTLSItemSecurity(title='TLS', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSPageNone(GuiVTLSPageXXX):
|
||||
"""Represent GUI vtls page none."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageNone."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiVTLSItemSecurity(title='TLS', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSPageTLS(GuiVTLSPageXXX):
|
||||
"""Represent GUI vtls page TLS."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageTLS."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def setupLayout(self):
|
||||
"""Set up layout."""
|
||||
layout = QFormLayout()
|
||||
layout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
|
||||
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||
|
||||
basicLayout = QGridLayout()
|
||||
basicLayout.setColumnStretch(1, 1)
|
||||
basicLayout.setColumnStretch(3, 1)
|
||||
|
||||
def addPair(index: int, row: int, column: int):
|
||||
"""Add pair."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
basicLayout.addWidget(label, row, column)
|
||||
basicLayout.addWidget(inputWidget, row, column + 1)
|
||||
|
||||
def addBasicFullRow(index: int, row: int):
|
||||
"""Add basic full row."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
basicLayout.addWidget(label, row, 0)
|
||||
basicLayout.addWidget(inputWidget, row, 1, 1, 3)
|
||||
|
||||
def addFullRow(index: int):
|
||||
"""Add full row."""
|
||||
widgets = self._containers[index].widgets()
|
||||
|
||||
if len(widgets) == 1:
|
||||
layout.addRow(widgets[0])
|
||||
else:
|
||||
label, inputWidget = widgets
|
||||
|
||||
layout.addRow(label, inputWidget)
|
||||
|
||||
addPair(0, 0, 0)
|
||||
addPair(2, 0, 2)
|
||||
addPair(1, 1, 0)
|
||||
addPair(3, 1, 2)
|
||||
addBasicFullRow(4, 2)
|
||||
|
||||
layout.addRow(basicLayout)
|
||||
|
||||
addFullRow(5)
|
||||
addFullRow(6)
|
||||
addFullRow(7)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiVTLSItemSecurity(title='TLS', translatable=False),
|
||||
GuiVTLSItemTLSServerName(title='SNI', translatable=False),
|
||||
GuiVTLSItemTLSFingerprint(title='Fingerprint', translatable=False),
|
||||
GuiVTLSItemTLSAlpn(title='Alpn', translatable=False),
|
||||
GuiVTLSItemTLSXXXTextInput(
|
||||
title='EchConfigList',
|
||||
key='echConfigList',
|
||||
translatable=False,
|
||||
),
|
||||
GuiVTLSItemTLSXXXTextInput(
|
||||
title='VerifyPeerCertByName',
|
||||
key='verifyPeerCertByName',
|
||||
translatable=False,
|
||||
),
|
||||
GuiVTLSItemTLSXXXTextInput(
|
||||
title='PinnedPeerCertSha256',
|
||||
key='pinnedPeerCertSha256',
|
||||
translatable=False,
|
||||
),
|
||||
GuiVTLSItemTLSAllowInsecure(title='AllowInsecure', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSPageReality(GuiVTLSPageXXX):
|
||||
"""Represent GUI vtls page reality."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageReality."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def setupLayout(self):
|
||||
"""Set up layout."""
|
||||
layout = QGridLayout()
|
||||
layout.setColumnStretch(1, 1)
|
||||
layout.setColumnStretch(3, 1)
|
||||
layout.setHorizontalSpacing(12)
|
||||
|
||||
def addPair(index: int, row: int, column: int):
|
||||
"""Add pair."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
layout.addWidget(label, row, column)
|
||||
layout.addWidget(inputWidget, row, column + 1)
|
||||
|
||||
def addFullRow(index: int, row: int):
|
||||
"""Add full row."""
|
||||
label, inputWidget = self._containers[index].widgets()
|
||||
|
||||
layout.addWidget(label, row, 0)
|
||||
layout.addWidget(inputWidget, row, 1, 1, 3)
|
||||
|
||||
addPair(0, 0, 0)
|
||||
addPair(2, 0, 2)
|
||||
addPair(1, 1, 0)
|
||||
addPair(4, 1, 2)
|
||||
addFullRow(3, 2)
|
||||
addFullRow(5, 3)
|
||||
addFullRow(6, 4)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return the editor item containers in display order."""
|
||||
return [
|
||||
GuiVTLSItemSecurity(title='TLS', translatable=False),
|
||||
GuiVTLSItemRealityServerName(title='SNI', translatable=False),
|
||||
GuiVTLSItemRealityFingerprint(title='Fingerprint', translatable=False),
|
||||
GuiVTLSItemRealityPublicKey(title='PublicKey', translatable=False),
|
||||
GuiVTLSItemRealityShortId(title='ShortId', translatable=False),
|
||||
GuiVTLSItemRealityMldsa65Verify(title='Mldsa65Verify', translatable=False),
|
||||
GuiVTLSItemRealitySpiderX(title='SpiderX', translatable=False),
|
||||
]
|
||||
|
||||
|
||||
class GuiVTLSPageStackedWidget(QStackedWidget):
|
||||
"""Provide the GUI vtls page stacked widget."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the GuiVTLSPageStackedWidget."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Corresponds to stream security
|
||||
self._pages = [
|
||||
GuiVTLSPageEmpty(),
|
||||
GuiVTLSPageNone(),
|
||||
GuiVTLSPageTLS(),
|
||||
GuiVTLSPageReality(),
|
||||
]
|
||||
|
||||
for page in self._pages:
|
||||
self.addWidget(page)
|
||||
|
||||
def page(self, index: int) -> GuiVTLSPageXXX:
|
||||
"""Return the page value."""
|
||||
return self._pages[index]
|
||||
|
||||
def connectActivated(self, func: Callable):
|
||||
"""Connect activated."""
|
||||
for page in self._pages:
|
||||
page.connectActivated(func)
|
||||
|
||||
|
||||
class GuiVTLSQGroupBox(GuiEditorItemFactory, AppQGroupBox):
|
||||
"""Group the GUI vtlsq editor controls."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the GuiVTLSQGroupBox."""
|
||||
translatable = kwargs.pop('translatable', False)
|
||||
|
||||
super().__init__('TLS', **kwargs, translatable=translatable)
|
||||
|
||||
self._config = ConfigFactory()
|
||||
|
||||
self._widget = GuiVTLSPageStackedWidget()
|
||||
self._widget.connectActivated(self.handleActivated)
|
||||
|
||||
layout = QFormLayout()
|
||||
layout.addRow(self._widget)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def currentIndex(self) -> int:
|
||||
"""Return the current index value."""
|
||||
return self._widget.currentIndex()
|
||||
|
||||
def setCurrentIndex(self, index: int):
|
||||
"""Set current index."""
|
||||
self._widget.setCurrentIndex(index)
|
||||
|
||||
def page(self, index: int) -> GuiVTLSPageXXX:
|
||||
"""Return the page value."""
|
||||
return self._widget.page(index)
|
||||
|
||||
def handleActivated(self, index: int):
|
||||
"""Handle activated."""
|
||||
page = self.page(index)
|
||||
page.factoryToInput(self._config)
|
||||
page.setSecurityText(STREAM_SECURITY[index])
|
||||
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
def inputToFactory(self, config: ConfigFactory) -> bool:
|
||||
"""Apply the current editor value to the configuration."""
|
||||
return self.page(self.currentIndex()).inputToFactory(config)
|
||||
|
||||
def factoryToInput(self, config: ConfigFactory):
|
||||
# Shallow copy
|
||||
"""Load the configuration value into the editor."""
|
||||
self._config = config
|
||||
|
||||
streamSettings = ConfigXray.getProxyOutboundStream(config)
|
||||
security = streamSettings.get('security', '')
|
||||
|
||||
if not isinstance(security, str):
|
||||
return
|
||||
|
||||
try:
|
||||
index = STREAM_SECURITY.index(security)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
else:
|
||||
self.page(index).factoryToInput(config)
|
||||
self.setCurrentIndex(index)
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
+2
-1997
File diff suppressed because it is too large
Load Diff
@@ -22,17 +22,11 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Core import *
|
||||
from Furious.Core.CoreManager import CoreManager
|
||||
from Furious.TrayActions.Import import *
|
||||
from Furious.Widget.GuiHysteria1 import *
|
||||
from Furious.Widget.GuiHysteria2 import *
|
||||
from Furious.Widget.GuiShadowsocks import *
|
||||
from Furious.Widget.GuiSocks import *
|
||||
from Furious.Widget.GuiTrojan import *
|
||||
from Furious.Widget.GuiVLESS import *
|
||||
from Furious.Widget.GuiVMess import *
|
||||
from Furious.Widget.WaitingSpinner import *
|
||||
from Furious.Window.QRCodeWindow import *
|
||||
from Furious.Window.TextEditorWindow import *
|
||||
@@ -544,81 +538,25 @@ class TestDownloadSpeedWorker(WebGETManager):
|
||||
|
||||
pass
|
||||
|
||||
@functools.singledispatchmethod
|
||||
def _startCore(self, config) -> bool:
|
||||
"""Return the start core value used by the test download speed worker."""
|
||||
self.factory.setExtras('speedResult', 'Invalid')
|
||||
self.sync()
|
||||
"""Prepare and start a download test through the owning plugin."""
|
||||
plugin = getPluginRegistry().pluginForConfig(config)
|
||||
if plugin is None:
|
||||
self.factory.setExtras('speedResult', 'Invalid')
|
||||
self.sync()
|
||||
|
||||
# Unrecognized core. Return
|
||||
return False
|
||||
return False
|
||||
|
||||
configcopy = plugin.prepareDownloadTest(config, self.port)
|
||||
if configcopy is None:
|
||||
self.factory.setExtras('speedResult', 'Invalid')
|
||||
self.sync()
|
||||
|
||||
return False
|
||||
|
||||
@_startCore.register(ConfigXray)
|
||||
def _(self, config) -> bool:
|
||||
"""Handle the registered singledispatch variant."""
|
||||
self.factory.setExtras('speedResult', 'Starting')
|
||||
self.sync()
|
||||
|
||||
configcopy = config.deepcopy()
|
||||
# Force redirect
|
||||
configcopy['inbounds'] = [
|
||||
{
|
||||
'tag': 'http',
|
||||
'port': self.port,
|
||||
'listen': '127.0.0.1',
|
||||
'protocol': 'http',
|
||||
'sniffing': {
|
||||
'enabled': True,
|
||||
'destOverride': [
|
||||
'http',
|
||||
'tls',
|
||||
],
|
||||
},
|
||||
'settings': {
|
||||
'auth': 'noauth',
|
||||
'udp': True,
|
||||
'allowTransparent': False,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
try:
|
||||
for outboundObject in configcopy['outbounds']:
|
||||
if outboundObject['tag'] == f'proxy':
|
||||
# Avoid confusion with potentially existing 'proxy' tag
|
||||
outboundObject['tag'] = f'proxy{self.port}'
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
|
||||
return self.coreManager.start(
|
||||
configcopy,
|
||||
AppBuiltinRouting.Global.value,
|
||||
self.coreExitCallback,
|
||||
msgCallbackCore=self.coreMsgCallback,
|
||||
deepcopy=False,
|
||||
proxyModeOnly=True,
|
||||
log=False,
|
||||
)
|
||||
|
||||
@_startCore.register(ConfigHysteria1)
|
||||
@_startCore.register(ConfigHysteria2)
|
||||
def _(self, config) -> bool:
|
||||
"""Handle the registered singledispatch variant."""
|
||||
self.factory.setExtras('speedResult', 'Starting')
|
||||
self.sync()
|
||||
|
||||
configcopy = config.deepcopy()
|
||||
# Force redirect
|
||||
configcopy['http'] = {
|
||||
'listen': f'127.0.0.1:{self.port}',
|
||||
'timeout': 300,
|
||||
'disable_udp': False,
|
||||
}
|
||||
# No socks inbounds
|
||||
configcopy.pop('socks5', '')
|
||||
|
||||
return self.coreManager.start(
|
||||
configcopy,
|
||||
AppBuiltinRouting.Global.value,
|
||||
@@ -1872,51 +1810,27 @@ class UserServersQTableView(
|
||||
APP().systemTray.ConnectAction.doReconnect()
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def getGuiEditorByProtocol(self, protocol: Protocol, **kwargs):
|
||||
def getGuiEditorByProtocol(self, protocol, **kwargs):
|
||||
"""Return GUI editor by protocol."""
|
||||
logger.debug(f'getGuiEditorByProtocol called with protocol {protocol.value}')
|
||||
protocolId = getattr(protocol, 'value', protocol)
|
||||
logger.debug(f'getGuiEditorByProtocol called with protocol {protocolId}')
|
||||
|
||||
if protocol == Protocol.VMess:
|
||||
return GuiVMess(parent=self, **kwargs)
|
||||
if protocol == Protocol.VLESS:
|
||||
return GuiVLESS(parent=self, **kwargs)
|
||||
if protocol == Protocol.Shadowsocks:
|
||||
return GuiShadowsocks(parent=self, **kwargs)
|
||||
if protocol == Protocol.Socks:
|
||||
return GuiSocks(parent=self, **kwargs)
|
||||
if protocol == Protocol.Trojan:
|
||||
return GuiTrojan(parent=self, **kwargs)
|
||||
if protocol == Protocol.Hysteria2:
|
||||
return GuiHysteria2(parent=self, **kwargs)
|
||||
if protocol == Protocol.Hysteria1:
|
||||
return GuiHysteria1(parent=self, **kwargs)
|
||||
return getPluginRegistry().createEditorForProtocol(
|
||||
protocol,
|
||||
parent=self,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@functools.singledispatchmethod
|
||||
def getGuiEditorByFactory(
|
||||
self, factory, **kwargs
|
||||
) -> Union[GuiEditorWidgetQDialog, None]:
|
||||
"""Return GUI editor by factory."""
|
||||
return None
|
||||
|
||||
@getGuiEditorByFactory.register(ConfigXray)
|
||||
def _(self, factory, **kwargs):
|
||||
"""Handle the registered singledispatch variant."""
|
||||
return self.getGuiEditorByProtocol(
|
||||
Protocol.toEnum(factory.proxyProtocol), **kwargs
|
||||
return getPluginRegistry().createEditorForConfig(
|
||||
factory,
|
||||
parent=self,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@getGuiEditorByFactory.register(ConfigHysteria1)
|
||||
def _(self, factory, **kwargs):
|
||||
"""Handle the registered singledispatch variant."""
|
||||
return self.getGuiEditorByProtocol(Protocol.Hysteria1, **kwargs)
|
||||
|
||||
@getGuiEditorByFactory.register(ConfigHysteria2)
|
||||
def _(self, factory, **kwargs):
|
||||
"""Handle the registered singledispatch variant."""
|
||||
return self.getGuiEditorByProtocol(Protocol.Hysteria2, **kwargs)
|
||||
|
||||
@QtCore.Slot(QtCore.QModelIndex)
|
||||
def handleItemDoubleClicked(self, modelIndex: QtCore.QModelIndex):
|
||||
"""Handle item double clicked."""
|
||||
@@ -2078,7 +1992,7 @@ class UserServersQTableView(
|
||||
|
||||
def addServerViaGui(
|
||||
self,
|
||||
protocol: Protocol,
|
||||
protocol,
|
||||
windowTitle: str = APPLICATION_NAME,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
@@ -1,272 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide widgets for Xray asset viewer Qt list widget."""
|
||||
"""Provide compatibility imports for the official Xray asset viewer widget."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
import datetime
|
||||
import functools
|
||||
|
||||
__all__ = ['XrayAssetViewerQListWidget']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MBoxAssetExists(AppQMessageBox):
|
||||
"""Represent m box asset exists."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the MBoxAssetExists."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setStandardButtons(
|
||||
AppQMessageBox.StandardButton.Yes | AppQMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box asset exists."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(_(self.text()))
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
|
||||
"""Provide the Xray asset viewer Qt list widget."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the XrayAssetViewerQListWidget."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
self.setSelectionBehavior(AppQListWidget.SelectionBehavior.SelectRows)
|
||||
self.setSelectionMode(AppQListWidget.SelectionMode.ExtendedSelection)
|
||||
self.setIconSize(QtCore.QSize(64, 64))
|
||||
|
||||
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
|
||||
self.initialTheme = APP().theme()
|
||||
else:
|
||||
self.initialTheme = None
|
||||
|
||||
self.flushItem()
|
||||
|
||||
logger.info(f'Xray-core asset dir is \'{XRAY_ASSET_DIR}\'')
|
||||
|
||||
contextMenuActions = [
|
||||
AppQAction(
|
||||
_('Delete'),
|
||||
callback=lambda: self.deleteSelectedItem(),
|
||||
),
|
||||
]
|
||||
|
||||
self.contextMenu = AppQMenu(*contextMenuActions)
|
||||
|
||||
# Add actions to self in order to activate shortcuts
|
||||
self.addActions(self.contextMenu.actions())
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested)
|
||||
|
||||
@QtCore.Slot(QtCore.QPoint)
|
||||
def handleCustomContextMenuRequested(self, point):
|
||||
"""Handle custom context menu requested."""
|
||||
self.contextMenu.exec(self.viewport().mapToGlobal(point))
|
||||
|
||||
def flushItemByTheme(self, theme: str):
|
||||
"""Refresh item by theme."""
|
||||
self.clear()
|
||||
|
||||
maxlen = max(
|
||||
len(filename)
|
||||
for filename in os.listdir(XRAY_ASSET_DIR)
|
||||
if os.path.isfile(XRAY_ASSET_DIR / filename)
|
||||
)
|
||||
|
||||
for filename in os.listdir(XRAY_ASSET_DIR):
|
||||
if os.path.isfile(XRAY_ASSET_DIR / filename):
|
||||
try:
|
||||
# Exception may be raised
|
||||
epoch = os.path.getmtime(XRAY_ASSET_DIR / filename)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
mdate = ''
|
||||
else:
|
||||
mdate = datetime.datetime.fromtimestamp(epoch).strftime(
|
||||
'%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
item = QListWidgetItem(f'{filename:{maxlen + 6}}{mdate}')
|
||||
item.setFont(QFont(AppFontName()))
|
||||
|
||||
if AppSettings.isStateON_('DarkMode'):
|
||||
# Custom dark mode
|
||||
item.setIcon(bootstrapIconWhite('file-earmark.svg'))
|
||||
else:
|
||||
if theme == 'Dark':
|
||||
if PLATFORM == 'Windows':
|
||||
# Windows
|
||||
if versionToValue(PYSIDE6_VERSION) < versionToValue(
|
||||
'6.7.0'
|
||||
):
|
||||
# PySide6 < 6.7.0 has no system theme handling on Windows.
|
||||
# Always use black icon
|
||||
item.setIcon(bootstrapIcon('file-earmark.svg'))
|
||||
else:
|
||||
# PySide6 has system theme handling.
|
||||
item.setIcon(bootstrapIconWhite('file-earmark.svg'))
|
||||
else:
|
||||
item.setIcon(bootstrapIconWhite('file-earmark.svg'))
|
||||
else:
|
||||
item.setIcon(bootstrapIcon('file-earmark.svg'))
|
||||
|
||||
self.addItem(item)
|
||||
|
||||
def flushItem(self):
|
||||
"""Refresh item."""
|
||||
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
|
||||
assert self.initialTheme is not None
|
||||
|
||||
# Ubuntu 20.04. Flush by initial theme(Ubuntu 20.04 theme changes bug)
|
||||
self.flushItemByTheme(self.initialTheme)
|
||||
else:
|
||||
self.flushItemByTheme(APP().theme())
|
||||
|
||||
def appendNewItem(self, filename: str):
|
||||
"""Append new item."""
|
||||
|
||||
def append(_filename):
|
||||
"""Append the Xray asset viewer Qt list widget."""
|
||||
try:
|
||||
shutil.copy(_filename, XRAY_ASSET_DIR)
|
||||
except shutil.SameFileError:
|
||||
# Same file imported. Do nothing
|
||||
pass
|
||||
except Exception as ex:
|
||||
# Any non-exit exception
|
||||
|
||||
_mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
_mbox.setWindowTitle(_('Import'))
|
||||
_mbox.setText(_('Error import asset file'))
|
||||
_mbox.setInformativeText(str(ex))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
_mbox.open()
|
||||
else:
|
||||
self.flushItem()
|
||||
|
||||
_mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Information)
|
||||
_mbox.setWindowTitle(_('Import'))
|
||||
_mbox.setText(_('Import asset file success'))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
_mbox.open()
|
||||
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
if os.path.isfile(XRAY_ASSET_DIR / basename):
|
||||
|
||||
def handleResultCode(_filename, code):
|
||||
"""Handle result code."""
|
||||
if code == PySide6Legacy.enumValueWrapper(
|
||||
AppQMessageBox.StandardButton.Yes
|
||||
):
|
||||
append(_filename)
|
||||
else:
|
||||
# Do not overwrite
|
||||
pass
|
||||
|
||||
mbox = MBoxAssetExists(icon=AppQMessageBox.Icon.Question)
|
||||
mbox.setWindowTitle(_('Import'))
|
||||
mbox.setText(_('Asset file already exists. Overwrite?'))
|
||||
mbox.setInformativeText(basename)
|
||||
mbox.finished.connect(functools.partial(handleResultCode, filename))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
else:
|
||||
append(filename)
|
||||
|
||||
def deleteSelectedItem(self):
|
||||
"""Delete selected item."""
|
||||
indexes = self.selectedIndex
|
||||
|
||||
if len(indexes) == 0:
|
||||
# Nothing selected
|
||||
return
|
||||
|
||||
def handleResultCode(_indexes, code):
|
||||
"""Handle result code."""
|
||||
if code == PySide6Legacy.enumValueWrapper(
|
||||
AppQMessageBox.StandardButton.Yes
|
||||
):
|
||||
for index in _indexes:
|
||||
os.remove(XRAY_ASSET_DIR / self.item(index).text())
|
||||
|
||||
self.flushItem()
|
||||
else:
|
||||
# Do not delete
|
||||
pass
|
||||
|
||||
if PLATFORM == 'Windows':
|
||||
# Windows
|
||||
mbox = MBoxQuestionDelete(icon=AppQMessageBox.Icon.Question)
|
||||
else:
|
||||
# macOS & linux
|
||||
mbox = MBoxQuestionDelete(
|
||||
icon=AppQMessageBox.Icon.Question, parent=self.parent()
|
||||
)
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
mbox.isMulti = bool(len(indexes) > 1)
|
||||
mbox.possibleRemark = f'{self.item(indexes[0]).text()}'
|
||||
mbox.setText(mbox.customText())
|
||||
mbox.finished.connect(functools.partial(handleResultCode, indexes))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Handle a key press for the Xray asset viewer Qt list widget."""
|
||||
if event.key() == QtCore.Qt.Key.Key_Delete:
|
||||
self.deleteSelectedItem()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def themeChangedCallback(self, theme):
|
||||
"""Update the Xray asset viewer Qt list widget for a theme change."""
|
||||
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
|
||||
# Ubuntu 20.04 system dark theme does not
|
||||
# change menu color. Do nothing
|
||||
pass
|
||||
else:
|
||||
self.flushItemByTheme(theme)
|
||||
from Furious.Plugins.Official.Xray.XrayAssetViewerQListWidget import *
|
||||
|
||||
@@ -23,17 +23,9 @@ from .Application import *
|
||||
from .ConnectProgressBar import *
|
||||
from .GuiCustomizeNetworkTest import *
|
||||
from .GuiCustomizeProxyBypass import *
|
||||
from .GuiHysteria1 import *
|
||||
from .GuiHysteria2 import *
|
||||
from .GuiShadowsocks import *
|
||||
from .GuiSocks import *
|
||||
from .GuiTrojan import *
|
||||
from .GuiTUNSettings import *
|
||||
from .GuiVLESS import *
|
||||
from .GuiVMess import *
|
||||
from .IndentSpinBox import *
|
||||
from .SystemTrayIcon import *
|
||||
from .UserServersQTableView import *
|
||||
from .UserSubsQTableView import *
|
||||
from .WaitingSpinner import *
|
||||
from .XrayAssetViewerQListWidget import *
|
||||
|
||||
@@ -22,6 +22,7 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Widget.UserServersQTableView import *
|
||||
@@ -29,8 +30,6 @@ from Furious.Widget.GuiCustomizeNetworkTest import *
|
||||
from Furious.Widget.GuiCustomizeProxyBypass import *
|
||||
from Furious.Widget.GuiTUNSettings import *
|
||||
from Furious.Window.UserSubsWindow import *
|
||||
from Furious.Window.UserRoutingWindow import *
|
||||
from Furious.Window.XrayAssetViewerWindow import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
@@ -284,8 +283,13 @@ class AppMainWindow(AppQMainWindow):
|
||||
showProgress=False,
|
||||
),
|
||||
)
|
||||
self.userRoutingWindow = UserRoutingWindow(parent=self)
|
||||
self.xrayAssetViewerWindow = XrayAssetViewerWindow(parent=self)
|
||||
pluginRegistry = getPluginRegistry()
|
||||
pluginRoutingEditors = pluginRegistry.routingEditors(parent=self)
|
||||
self.pluginRoutingEditors = tuple(
|
||||
editor for _plugin, editor in pluginRoutingEditors
|
||||
)
|
||||
if self.pluginRoutingEditors:
|
||||
self.userRoutingWindow = self.pluginRoutingEditors[0]
|
||||
self.customizeProxyBypassDialog = GuiCustomizeProxyBypassDialog(parent=self)
|
||||
self.customizeNetworkTestDialog = GuiCustomizeNetworkTestDialog(parent=self)
|
||||
|
||||
@@ -322,57 +326,31 @@ class AppMainWindow(AppQMainWindow):
|
||||
),
|
||||
]
|
||||
|
||||
serverActions = [
|
||||
AppQAction(
|
||||
_('Add VMess Server...'),
|
||||
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
||||
Protocol.VMess, _('Add VMess Server...')
|
||||
),
|
||||
),
|
||||
AppQAction(
|
||||
_('Add VLESS Server...'),
|
||||
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
||||
Protocol.VLESS, _('Add VLESS Server...')
|
||||
),
|
||||
),
|
||||
AppQAction(
|
||||
_('Add Shadowsocks Server...'),
|
||||
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
||||
Protocol.Shadowsocks, _('Add Shadowsocks Server...')
|
||||
),
|
||||
),
|
||||
AppQAction(
|
||||
_('Add Trojan Server...'),
|
||||
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
||||
Protocol.Trojan, _('Add Trojan Server...')
|
||||
),
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Add Hysteria1 Server...'),
|
||||
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
||||
Protocol.Hysteria1, _('Add Hysteria1 Server...')
|
||||
),
|
||||
),
|
||||
AppQAction(
|
||||
_('Add Hysteria2 Server...'),
|
||||
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
||||
Protocol.Hysteria2, _('Add Hysteria2 Server...')
|
||||
),
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Add SOCKS Server...'),
|
||||
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
||||
Protocol.Socks, _('Add SOCKS Server...')
|
||||
),
|
||||
),
|
||||
AppQSeperator(),
|
||||
serverActions = []
|
||||
for descriptor in pluginRegistry.protocolDescriptors():
|
||||
if descriptor.separatorBefore and serverActions:
|
||||
serverActions.append(AppQSeperator())
|
||||
|
||||
actionText = _(descriptor.addActionText)
|
||||
serverActions.append(
|
||||
AppQAction(
|
||||
actionText,
|
||||
callback=functools.partial(
|
||||
self.userServersQTableWidget.addServerViaGui,
|
||||
descriptor.id,
|
||||
actionText,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if serverActions:
|
||||
serverActions.append(AppQSeperator())
|
||||
serverActions.append(
|
||||
AppQAction(
|
||||
_('New Empty Configuration'),
|
||||
callback=lambda: self.userServersQTableWidget.newEmptyItem(),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
subsActions = [
|
||||
AppQAction(
|
||||
@@ -448,6 +426,7 @@ class AppMainWindow(AppQMainWindow):
|
||||
AppQSeperator(),
|
||||
]
|
||||
|
||||
pluginTools = pluginRegistry.mainWindowTools(parent=self)
|
||||
toolsActions = [
|
||||
*customizeTUNSettingsAction,
|
||||
AppQAction(
|
||||
@@ -464,11 +443,41 @@ class AppMainWindow(AppQMainWindow):
|
||||
AppQSeperator(),
|
||||
*restartAsAdminAction,
|
||||
*openAppFolderAction,
|
||||
AppQAction(
|
||||
_('Manage Xray-core Asset File...'),
|
||||
callback=lambda: self.xrayAssetViewerWindow.show(),
|
||||
),
|
||||
]
|
||||
if pluginTools:
|
||||
toolsActions.extend(pluginTools)
|
||||
|
||||
routingActions = [
|
||||
AppQAction(
|
||||
_(plugin.displayName),
|
||||
callback=editor.show,
|
||||
)
|
||||
for plugin, editor in pluginRoutingEditors
|
||||
]
|
||||
if len(routingActions) == 1:
|
||||
routingToolbarActions = [
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Routing'),
|
||||
icon=bootstrapIcon('signpost.svg'),
|
||||
checkable=False,
|
||||
callback=routingActions[0].trigger,
|
||||
),
|
||||
]
|
||||
elif routingActions:
|
||||
routingToolbarActions = [
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Routing'),
|
||||
icon=bootstrapIcon('signpost.svg'),
|
||||
menu=AppQMenu(*routingActions),
|
||||
useSetMenu=False,
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
),
|
||||
]
|
||||
else:
|
||||
routingToolbarActions = []
|
||||
|
||||
if hasattr(AppQAction, 'setMenu'):
|
||||
self.toolbar = AppQToolBar(
|
||||
@@ -498,13 +507,7 @@ class AppMainWindow(AppQMainWindow):
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Routing'),
|
||||
icon=bootstrapIcon('signpost.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.userRoutingWindow.show(),
|
||||
),
|
||||
*routingToolbarActions,
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Tools'),
|
||||
@@ -552,6 +555,15 @@ class AppMainWindow(AppQMainWindow):
|
||||
'actions': [*subsActions],
|
||||
}
|
||||
|
||||
routingMenus = []
|
||||
if routingActions:
|
||||
routingMenus.append(
|
||||
{
|
||||
'name': 'Routing',
|
||||
'actions': [*routingActions],
|
||||
}
|
||||
)
|
||||
|
||||
toolsMenu = {
|
||||
'name': 'Tools',
|
||||
'actions': [*toolsActions],
|
||||
@@ -581,12 +593,20 @@ class AppMainWindow(AppQMainWindow):
|
||||
_('Log'),
|
||||
_('Server'),
|
||||
_('Subscription'),
|
||||
_('Routing'),
|
||||
_('Tools'),
|
||||
_('Help'),
|
||||
]
|
||||
|
||||
# Menus
|
||||
for menuDict in (logMenu, serverMenu, subsMenu, toolsMenu, helpMenu):
|
||||
for menuDict in (
|
||||
logMenu,
|
||||
serverMenu,
|
||||
subsMenu,
|
||||
*routingMenus,
|
||||
toolsMenu,
|
||||
helpMenu,
|
||||
):
|
||||
menuName = menuDict['name']
|
||||
menuObjName = f'_{menuName}Menu'
|
||||
menu = AppQMenu(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,113 +1,5 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Provide the application window for Xray asset viewer window."""
|
||||
"""Provide compatibility imports for the official Xray asset viewer window."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Widget.XrayAssetViewerQListWidget import *
|
||||
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import logging
|
||||
|
||||
__all__ = ['XrayAssetViewerWindow']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XrayAssetViewerWindow(AppQMainWindow):
|
||||
"""Present the Xray asset viewer window."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the XrayAssetViewerWindow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle(_('Xray-core Asset File'))
|
||||
|
||||
self.xrayAssetViewerWidget = XrayAssetViewerQListWidget(parent=self)
|
||||
self.setCentralWidget(self.xrayAssetViewerWidget)
|
||||
|
||||
if versionToValue(PYSIDE6_VERSION) <= versionToValue('6.1.3'):
|
||||
openAssetDirectoryActions = []
|
||||
else:
|
||||
# openUrl will crash on PySide6 6.1.3, probably a Qt bug
|
||||
openAssetDirectoryActions = [
|
||||
AppQAction(
|
||||
_('Open Asset Directory'),
|
||||
callback=lambda: self.openAssetDirectory(),
|
||||
shortcut=QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier.ControlModifier,
|
||||
QtCore.Qt.Key.Key_O,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
self.fileMenu = AppQMenu(
|
||||
AppQAction(
|
||||
_('Refresh'),
|
||||
callback=lambda: self.flushItem(),
|
||||
shortcut=QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier.ControlModifier,
|
||||
QtCore.Qt.Key.Key_R,
|
||||
),
|
||||
),
|
||||
AppQSeperator(),
|
||||
*openAssetDirectoryActions,
|
||||
AppQAction(
|
||||
_('Import From File...'),
|
||||
callback=lambda: self.appendNewItem(),
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Exit'),
|
||||
callback=lambda: self.close(),
|
||||
),
|
||||
title=_('File'),
|
||||
parent=self.menuBar(),
|
||||
)
|
||||
|
||||
self.menuBar().addMenu(self.fileMenu)
|
||||
|
||||
def setWidthAndHeight(self):
|
||||
"""Apply the default size for the Xray asset viewer window."""
|
||||
self.resize(360, 360 * GOLDEN_RATIO)
|
||||
|
||||
def flushItem(self):
|
||||
"""Refresh item."""
|
||||
self.xrayAssetViewerWidget.flushItem()
|
||||
|
||||
@staticmethod
|
||||
def openAssetDirectory():
|
||||
"""Open asset directory."""
|
||||
if QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(XRAY_ASSET_DIR)):
|
||||
logger.info(f'open Xray-core asset dir success')
|
||||
else:
|
||||
logger.error(f'open Xray-core asset dir failed')
|
||||
|
||||
def appendNewItem(self):
|
||||
"""Append new item."""
|
||||
filename, selectedFilter = QFileDialog.getOpenFileName(
|
||||
None, _('Import File'), filter=_('All files (*)')
|
||||
)
|
||||
|
||||
if filename:
|
||||
self.xrayAssetViewerWidget.appendNewItem(filename)
|
||||
from Furious.Plugins.Official.Xray.XrayAssetViewerWindow import *
|
||||
|
||||
@@ -24,5 +24,3 @@ from .LogViewerWindow import *
|
||||
from .QRCodeWindow import *
|
||||
from .TextEditorWindow import *
|
||||
from .UserSubsWindow import *
|
||||
from .UserRoutingWindow import *
|
||||
from .XrayAssetViewerWindow import *
|
||||
|
||||
Reference in New Issue
Block a user