Introduce extensible core plugin architecture

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-12 12:20:15 +08:00
parent a3ec887f26
commit b61f9ebef2
58 changed files with 11572 additions and 10801 deletions
+12 -317
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 *
-3
View File
@@ -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
+1 -1
View File
@@ -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']
+2 -1
View File
@@ -19,6 +19,7 @@
from __future__ import annotations
from Furious.Plugins.Configuration import *
from .Encoder import *
from .Storage import *
from .Configuration import *
+98
View File
@@ -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."""
+52
View File
@@ -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
+160
View File
@@ -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 *
+117
View File
@@ -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 *
+180
View File
@@ -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(),
]
+180
View File
@@ -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(),
]
+181
View File
@@ -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(),
]
+380
View File
@@ -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(),
]
+389
View File
@@ -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(),
]
+803
View File
@@ -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
+398
View File
@@ -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 *
+16
View File
@@ -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())
+350
View File
@@ -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)
+7
View File
@@ -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 *
+5 -8
View File
@@ -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',
+2 -233
View File
@@ -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 *
-1
View File
@@ -32,4 +32,3 @@ from .TextEditor import *
from .TextEditorTheme import *
from .UpdatesManager import *
from .WebGETManager import *
from .XrayAssetDownloadManager import *
+11 -23
View File
@@ -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(
+5 -8
View File
@@ -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)
+3 -2
View File
@@ -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
+2 -383
View File
@@ -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 *
+2 -888
View File
@@ -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 *
+2 -230
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 *
File diff suppressed because it is too large Load Diff
+28 -114
View File
@@ -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,
):
+2 -269
View File
@@ -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 *
-8
View File
@@ -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 *
+84 -64
View File
@@ -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
+2 -110
View File
@@ -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 *
-2
View File
@@ -24,5 +24,3 @@ from .LogViewerWindow import *
from .QRCodeWindow import *
from .TextEditorWindow import *
from .UserSubsWindow import *
from .UserRoutingWindow import *
from .XrayAssetViewerWindow import *