diff --git a/Furious/Externals/GenTranslation.py b/Furious/Externals/GenTranslation.py index d4b48a5..6ce5015 100644 --- a/Furious/Externals/GenTranslation.py +++ b/Furious/Externals/GenTranslation.py @@ -441,6 +441,7 @@ TRANSLATION = { "Furious.Widget.GuiHysteria1", "Furious.Widget.GuiHysteria2", "Furious.Widget.GuiShadowsocks", + "Furious.Widget.GuiSocks", "Furious.Widget.GuiTrojan", "Furious.Widget.GuiVLESS", "Furious.Widget.GuiVMess", @@ -464,6 +465,7 @@ TRANSLATION = { "Address": { "source": [ "Furious.Widget.GuiShadowsocks", + "Furious.Widget.GuiSocks", "Furious.Widget.GuiTrojan", "Furious.Widget.GuiVLESS", "Furious.Widget.GuiVMess", @@ -476,6 +478,7 @@ TRANSLATION = { "Port": { "source": [ "Furious.Widget.GuiShadowsocks", + "Furious.Widget.GuiSocks", "Furious.Widget.GuiTrojan", "Furious.Widget.GuiVLESS", "Furious.Widget.GuiVMess", @@ -1265,6 +1268,7 @@ TRANSLATION = { "Furious.Widget.GuiHysteria1", "Furious.Widget.GuiHysteria2", "Furious.Widget.GuiShadowsocks", + "Furious.Widget.GuiSocks", "Furious.Widget.GuiTrojan", "Furious.Widget.GuiTUNSettings", "Furious.Widget.GuiVLESS", @@ -1279,6 +1283,7 @@ TRANSLATION = { "Furious.Widget.GuiHysteria1", "Furious.Widget.GuiHysteria2", "Furious.Widget.GuiShadowsocks", + "Furious.Widget.GuiSocks", "Furious.Widget.GuiTrojan", "Furious.Widget.GuiVLESS", "Furious.Widget.GuiVMess" @@ -1317,6 +1322,7 @@ TRANSLATION = { "Password": { "source": [ "Furious.Widget.GuiShadowsocks", + "Furious.Widget.GuiSocks", "Furious.Widget.GuiTrojan" ], "RU": "Пароль", @@ -2173,5 +2179,21 @@ TRANSLATION = { "RU": "Сгенерировать", "ZH": "生成", "isReviewed": "True" + }, + "Username": { + "source": [ + "Furious.Widget.GuiSocks" + ], + "RU": "Имя пользователя", + "ZH": "用户名", + "isReviewed": "True" + }, + "Add SOCKS Server...": { + "source": [ + "Furious.Window.AppMainWindow" + ], + "RU": "Добавить сервер SOCKS...", + "ZH": "添加SOCKS服务器...", + "isReviewed": "True" } } diff --git a/Furious/Frozenlib/Utility.py b/Furious/Frozenlib/Utility.py index 5548686..cdac142 100644 --- a/Furious/Frozenlib/Utility.py +++ b/Furious/Frozenlib/Utility.py @@ -51,6 +51,7 @@ class Protocol(Enum): VMess = 'VMess' VLESS = 'VLESS' Shadowsocks = 'Shadowsocks' + Socks = 'SOCKS' Trojan = 'Trojan' Hysteria1 = 'hysteria1' Hysteria2 = 'hysteria2' @@ -70,6 +71,9 @@ class Protocol(Enum): if protocol.lower() == 'shadowsocks': return Protocol.Shadowsocks + if protocol.lower() == 'socks': + return Protocol.Socks + if protocol.lower() == 'trojan': return Protocol.Trojan diff --git a/Furious/Library/Configuration.py b/Furious/Library/Configuration.py index 23544b1..153e76b 100644 --- a/Furious/Library/Configuration.py +++ b/Furious/Library/Configuration.py @@ -174,6 +174,33 @@ class ConfigXrayProxyOutboundObjectSS(dict): ) +class ConfigXrayProxyOutboundObjectSocks(dict): + def __init__(self, address, port, user='', password=''): + settings = { + 'address': address, + 'port': int(port), + } + + if user != '' or password != '': + settings['user'] = user + settings['pass'] = password + + super().__init__( + **{ + 'tag': 'proxy', + 'protocol': 'socks', + 'settings': settings, + 'streamSettings': { + 'network': 'tcp', + }, + 'mux': { + 'enabled': False, + 'concurrency': -1, + }, + } + ) + + class ConfigXrayProxyOutboundObjectTrojan(dict): def __init__(self, password, address, port, type_, security, **kwargs): networkObjectArgs, securityArgs, TLSObjectArgs = {}, {}, {} @@ -353,6 +380,8 @@ class ConfigXray(ConfigFactory): try: if value == 'vless' or value == 'vmess': server = proxyOutbound['settings']['vnext'][0] + elif value == 'socks': + server = proxyOutbound['settings'] elif value == 'shadowsocks' or value == 'trojan': server = proxyOutbound['settings']['servers'][0] else: @@ -411,6 +440,9 @@ class ConfigXray(ConfigFactory): if proxyProtocol == 'vmess' or proxyProtocol == 'vless': return self.proxyOutboundObject['settings']['vnext'][0] + if proxyProtocol == 'socks': + return self.proxyOutboundObject['settings'] + if proxyProtocol == 'shadowsocks' or proxyProtocol == 'trojan': return self.proxyOutboundObject['settings']['servers'][0] @@ -423,6 +455,9 @@ class ConfigXray(ConfigFactory): @property def proxyUserObject(self) -> dict: try: + if self.proxyProtocol.lower() == 'socks': + return self.proxyServerObject + return self.proxyServerObject['users'][0] except Exception: # Any non-exit exceptions @@ -1224,6 +1259,24 @@ class ConfigXray(ConfigFactory): ConfigXrayProxyOutboundObjectSS(*getSSParams()), ) + @staticmethod + def URI2ProxyOutboundObjectSocks(URI: str) -> Tuple[str, dict]: + result = urlparse(URI) + remark = unquote(result.fragment) + + address = result.hostname or '' + port = result.port or 0 + user = unquote(result.username or '') + password = unquote(result.password or '') + + if address == '' or port == 0: + raise ValueError(f'Invalid SOCKS URI format {URI}') + + return ( + remark, + ConfigXrayProxyOutboundObjectSocks(address, port, user, password), + ) + @staticmethod def URI2ProxyOutboundObjectTrojan(URI: str) -> Tuple[str, dict]: result = urlparse(URI) @@ -1256,6 +1309,9 @@ class ConfigXray(ConfigFactory): if URI.startswith('ss://'): return ConfigXray.URI2ProxyOutboundObjectSS(URI) + if URI.startswith(('socks://', 'socks5://', 'socks5h://')): + return ConfigXray.URI2ProxyOutboundObjectSocks(URI) + if URI.startswith('trojan://'): return ConfigXray.URI2ProxyOutboundObjectTrojan(URI) @@ -1378,6 +1434,20 @@ class ConfigXray(ConfigFactory): return urlunparse(['ss', netloc, '', '', '', quote(override)]) + if protocol == 'socks': + address, port = list( + self.proxyServerObject[value] for value in ['address', 'port'] + ) + user = self.proxyUserObject.get('user', '') + password = self.proxyUserObject.get('pass', '') + + if user != '' or password != '': + netloc = f'{quote(user)}:{quote(password)}@{address}:{port}' + else: + netloc = f'{address}:{port}' + + return urlunparse(['socks5', netloc, '', '', '', quote(override)]) + if protocol == 'trojan': password, address, port = list( self.proxyServerObject[value] @@ -2185,6 +2255,22 @@ def configXrayEmptyProxyOutboundObject(protocol: Protocol) -> dict: 'concurrency': -1, }, } + elif value == 'socks': + return { + 'tag': 'proxy', + 'protocol': 'socks', + 'settings': { + 'address': '', + 'port': 0, + }, + 'streamSettings': { + 'network': 'tcp', + }, + 'mux': { + 'enabled': False, + 'concurrency': -1, + }, + } elif value == 'trojan': return { 'tag': 'proxy', @@ -2266,6 +2352,9 @@ def configFactoryFromAny(config: Union[str, dict], **kwargs) -> ConfigFactory: 'vless://', 'ss://', 'trojan://', + 'socks://', + 'socks5://', + 'socks5h://', ) ): return ConfigXray(config, **kwargs) @@ -2309,6 +2398,16 @@ def configFactoryBlank(protocol: Protocol) -> ConfigFactory: return factory + if protocol == Protocol.Socks: + factory = configFactoryFromDict(BLANK_CONFIG_XRAY) + factory['outbounds'][0]['protocol'] = protocol.value.lower() + factory['outbounds'][0]['settings'] = { + 'address': '', + 'port': 0, + } + + return factory + if protocol == Protocol.Shadowsocks or protocol == Protocol.Trojan: factory = configFactoryFromDict(BLANK_CONFIG_XRAY) factory['outbounds'][0]['protocol'] = protocol.value.lower() diff --git a/Furious/Widget/GuiSocks.py b/Furious/Widget/GuiSocks.py new file mode 100644 index 0000000..27457e0 --- /dev/null +++ b/Furious/Widget/GuiSocks.py @@ -0,0 +1,152 @@ +# Copyright (C) 2024–present Loren Eteval & contributors +# +# 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 . + +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): + def __init__(self, *args, **kwargs): + key = kwargs.pop('key', '') + + super().__init__(*args, **kwargs) + + self.key = key + + def inputToFactory(self, config: ConfigFactory) -> bool: + 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): + try: + proxyOutboundServer = getProxyOutboundServer(config) + + self.setText(proxyOutboundServer.get(self.key, '')) + except Exception: + # Any non-exit exceptions + + self.setText('') + + +class GuiSocksItemBasicPort(GuiEditorItemTextSpinBox): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.setRange(0, 65535) + + def inputToFactory(self, config: ConfigFactory) -> bool: + 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): + try: + proxyOutboundServer = getProxyOutboundServer(config) + + self.setValue(proxyOutboundServer.get('port', 0)) + except Exception: + # Any non-exit exceptions + + self.setValue(0) + + +class GuiSocksGroupBoxBasic(GuiEditorWidgetQGroupBox): + def __init__(self, **kwargs): + super().__init__(_('Basic Configuration'), **kwargs) + + def containerSequence(self): + 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): + def __init__(self, **kwargs): + super().__init__(_('Proxy'), **kwargs) + + def containerSequence(self): + return [ + GuiEditorItemProxyHttp(title='http', translatable=False), + GuiEditorItemProxySocks(title='socks', translatable=False), + ] + + +class GuiSocks(GuiEditorWidgetQDialog): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.setTabText(Protocol.Socks.value) + + @functools.lru_cache(None) + def groupBoxSequence(self): + return [ + GuiSocksGroupBoxBasic(), + GuiSocksGroupBoxProxy(), + GuiVTransportQGroupBox(), + GuiVTLSQGroupBox(), + ] diff --git a/Furious/Widget/UserServersQTableView.py b/Furious/Widget/UserServersQTableView.py index c4ab426..138e15c 100644 --- a/Furious/Widget/UserServersQTableView.py +++ b/Furious/Widget/UserServersQTableView.py @@ -27,6 +27,7 @@ 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 * @@ -1762,6 +1763,8 @@ class UserServersQTableView( 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: diff --git a/Furious/Widget/__init__.py b/Furious/Widget/__init__.py index 4ffe312..fbf46ec 100644 --- a/Furious/Widget/__init__.py +++ b/Furious/Widget/__init__.py @@ -24,6 +24,7 @@ from .GuiCustomizeProxyBypass import * from .GuiHysteria1 import * from .GuiHysteria2 import * from .GuiShadowsocks import * +from .GuiSocks import * from .GuiTrojan import * from .GuiTUNSettings import * from .GuiVLESS import * diff --git a/Furious/Window/AppMainWindow.py b/Furious/Window/AppMainWindow.py index fff0a09..52cb1b0 100644 --- a/Furious/Window/AppMainWindow.py +++ b/Furious/Window/AppMainWindow.py @@ -332,6 +332,13 @@ class AppMainWindow(AppQMainWindow): ), ), AppQSeperator(), + AppQAction( + _('Add SOCKS Server...'), + callback=lambda: self.userServersQTableWidget.addServerViaGui( + Protocol.Socks, _('Add SOCKS Server...') + ), + ), + AppQSeperator(), AppQAction( _('New Empty Configuration'), callback=lambda: self.userServersQTableWidget.newEmptyItem(),