mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-29 02:18:02 +03:00
Feature: added SOCKS protocol support
Signed-off-by: LorenEteval <loren.eteval@proton.me>
This commit is contained in:
committed by
Loren Eteval
parent
b2ff2ffd3c
commit
9c7ca9611c
Vendored
+22
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# 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/>.
|
||||
|
||||
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(),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user