mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-03 04:17:58 +03:00
Add support for shadowsocks import/export
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -2,11 +2,13 @@ from Furious.Core.Configuration import (
|
||||
Configuration,
|
||||
XrayCoreConfiguration,
|
||||
ProxyOutboundObject,
|
||||
ProxyOutboundObjectSS,
|
||||
)
|
||||
from Furious.Gui.Action import Action
|
||||
from Furious.Widget.Widget import Menu, MessageBox
|
||||
from Furious.Utility.Utility import (
|
||||
Base64Encoder,
|
||||
Protocol,
|
||||
StateContext,
|
||||
Storage,
|
||||
bootstrapIcon,
|
||||
@@ -219,7 +221,7 @@ class ImportLinkAction(Action):
|
||||
|
||||
remark = urllib.parse.unquote(parseResult.fragment)
|
||||
|
||||
uuid_, remote_host, remote_port = re.split(r'[@:]', parseResult.path)
|
||||
uuid_, remote_host, remote_port = re.split(r'[@:]', parseResult.netloc)
|
||||
|
||||
encryption = queryObject.get('encryption', 'none')
|
||||
type_ = queryObject.get('type', 'tcp')
|
||||
@@ -262,6 +264,83 @@ class ImportLinkAction(Action):
|
||||
|
||||
return '', False
|
||||
|
||||
@staticmethod
|
||||
def parseShareLinkSIP002(data):
|
||||
try:
|
||||
result = urllib.parse.urlparse(data)
|
||||
remark = urllib.parse.unquote(result.fragment)
|
||||
|
||||
try:
|
||||
# Try pack with 3 element
|
||||
userinfo, address, port = re.split(r'[@:]', result.netloc)
|
||||
|
||||
# Some old SS share link doesn't add padding
|
||||
# in base64 encoding. Add padding to userinfo
|
||||
method, password = (
|
||||
Base64Encoder.decode(userinfo + '===').decode().split(':')
|
||||
)
|
||||
except ValueError:
|
||||
# Unpack error. Try pack with 4 element
|
||||
|
||||
method, password, address, port = re.split(r'[@:]', result.netloc)
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
raise ex
|
||||
|
||||
myJSON = XrayCoreConfiguration.build(
|
||||
ProxyOutboundObjectSS(
|
||||
urllib.parse.unquote(method),
|
||||
urllib.parse.unquote(password),
|
||||
address,
|
||||
port,
|
||||
)
|
||||
)
|
||||
|
||||
QApplication.instance().MainWidget.importServer(
|
||||
remark,
|
||||
ujson.dumps(
|
||||
myJSON, indent=2, ensure_ascii=False, escape_forward_slashes=False
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'import share link from clipboard success. '
|
||||
f'Remark: {remark}. Protocol: {Protocol.Shadowsocks}'
|
||||
)
|
||||
|
||||
return remark, True
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return '', False
|
||||
|
||||
def parseShareLinkSS(self, data):
|
||||
try:
|
||||
# ss://base64...
|
||||
myData = Base64Encoder.decode(data[5:]).decode()
|
||||
myJSON = XrayCoreConfiguration.build(
|
||||
ProxyOutboundObjectSS(*re.split(r'[@:]', myData))
|
||||
)
|
||||
|
||||
QApplication.instance().MainWidget.importServer(
|
||||
'sslegacy',
|
||||
ujson.dumps(
|
||||
myJSON, indent=2, ensure_ascii=False, escape_forward_slashes=False
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'import share link from clipboard success. '
|
||||
f'Remark: sslegacy. Protocol: {Protocol.Shadowsocks}'
|
||||
)
|
||||
|
||||
return 'sslegacy', True
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return self.parseShareLinkSIP002(data)
|
||||
|
||||
def parseShareLink(self, shareLink):
|
||||
try:
|
||||
myHead, myBody = shareLink.split('://')
|
||||
@@ -277,12 +356,15 @@ class ImportLinkAction(Action):
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return self.parseShareLinkVMess(myBody, isV2rayN=False)
|
||||
return self.parseShareLinkVMess(shareLink, isV2rayN=False)
|
||||
else:
|
||||
return self.parseShareLinkVMess(myJSON, isV2rayN=True)
|
||||
|
||||
if myHead.lower() == 'vless':
|
||||
return self.parseShareLinkStandard('vless', myBody)
|
||||
return self.parseShareLinkStandard('vless', shareLink)
|
||||
|
||||
if myHead.lower() == 'ss':
|
||||
return self.parseShareLinkSS(shareLink)
|
||||
|
||||
return '', False
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from Furious.Core.Core import XrayCore
|
||||
from Furious.Core.Intellisense import Intellisense
|
||||
from Furious.Widget.Widget import MessageBox
|
||||
from Furious.Utility.Constants import APPLICATION_NAME
|
||||
from Furious.Utility.Utility import Base64Encoder, bootstrapIcon
|
||||
from Furious.Utility.Utility import Base64Encoder, Protocol, bootstrapIcon
|
||||
from Furious.Utility.Translator import gettext as _
|
||||
|
||||
import copy
|
||||
@@ -10,7 +10,8 @@ import ujson
|
||||
import functools
|
||||
import urllib.parse
|
||||
|
||||
quote = functools.partial(urllib.parse.quote)
|
||||
# '/' will be quoted for V2rayN compatibility.
|
||||
quote = functools.partial(urllib.parse.quote, safe='')
|
||||
unquote = functools.partial(urllib.parse.unquote)
|
||||
urlunparse = functools.partial(urllib.parse.urlunparse)
|
||||
|
||||
@@ -66,10 +67,10 @@ class Configuration:
|
||||
|
||||
if netType == 'tcp':
|
||||
try:
|
||||
if protocol.lower() == 'vmess':
|
||||
if protocol == Protocol.VMess:
|
||||
kwargs['type'] = netobj['header']['type']
|
||||
|
||||
if protocol.lower() == 'vless':
|
||||
if protocol == Protocol.VLESS:
|
||||
kwargs['headerType'] = netobj['header']['type']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
@@ -79,13 +80,13 @@ class Configuration:
|
||||
elif netType == 'kcp':
|
||||
try:
|
||||
# Get order matters here
|
||||
if protocol.lower() == 'vmess':
|
||||
if protocol == Protocol.VMess:
|
||||
if hasKey(netobj, 'seed'):
|
||||
kwargs['path'] = netobj['seed']
|
||||
|
||||
kwargs['type'] = netobj['header']['type']
|
||||
|
||||
if protocol.lower() == 'vless':
|
||||
if protocol == Protocol.VLESS:
|
||||
if hasKey(netobj, 'seed'):
|
||||
kwargs['seed'] = netobj['seed']
|
||||
|
||||
@@ -123,7 +124,7 @@ class Configuration:
|
||||
elif netType == 'quic':
|
||||
try:
|
||||
# Get order matters here
|
||||
if protocol.lower() == 'vmess':
|
||||
if protocol == Protocol.VMess:
|
||||
if hasKey(netobj, 'security'):
|
||||
kwargs['host'] = netobj['security']
|
||||
|
||||
@@ -132,7 +133,7 @@ class Configuration:
|
||||
|
||||
kwargs['type'] = netobj['header']['type']
|
||||
|
||||
if protocol.lower() == 'vless':
|
||||
if protocol == Protocol.VLESS:
|
||||
if hasKey(netobj, 'security'):
|
||||
kwargs['quicSecurity'] = netobj['security']
|
||||
|
||||
@@ -146,11 +147,11 @@ class Configuration:
|
||||
pass
|
||||
|
||||
elif netType == 'grpc':
|
||||
if protocol.lower() == 'vmess':
|
||||
if protocol == Protocol.VMess:
|
||||
if hasKey(netobj, 'serviceName'):
|
||||
kwargs['path'] = netobj['serviceName']
|
||||
|
||||
if protocol.lower() == 'vless':
|
||||
if protocol == Protocol.VLESS:
|
||||
if hasKey(netobj, 'serviceName'):
|
||||
kwargs['serviceName'] = netobj['serviceName']
|
||||
|
||||
@@ -190,9 +191,9 @@ class Configuration:
|
||||
return kwargs
|
||||
|
||||
# Begin export
|
||||
coreProtocol = Intellisense.getCoreProtocol(ob).lower()
|
||||
coreProtocol = Intellisense.getCoreProtocol(ob)
|
||||
|
||||
if coreProtocol == 'vmess' or coreProtocol == 'vless':
|
||||
if coreProtocol == Protocol.VMess or coreProtocol == Protocol.VLESS:
|
||||
proxyOutbound = None
|
||||
|
||||
for outbound in ob['outbounds']:
|
||||
@@ -211,7 +212,7 @@ class Configuration:
|
||||
proxyStreamNet = proxyStream['network']
|
||||
proxyStreamTLS = proxyStream['security']
|
||||
|
||||
if coreProtocol == 'vmess':
|
||||
if coreProtocol == Protocol.VMess:
|
||||
return (
|
||||
'vmess://'
|
||||
+ Base64Encoder.encode(
|
||||
@@ -238,7 +239,7 @@ class Configuration:
|
||||
).decode()
|
||||
)
|
||||
|
||||
if coreProtocol == 'vless':
|
||||
if coreProtocol == Protocol.VLESS:
|
||||
flowArg = {}
|
||||
|
||||
if proxyServerUser.get('flow'):
|
||||
@@ -262,11 +263,42 @@ class Configuration:
|
||||
)
|
||||
|
||||
return urlunparse(['vless', netloc, '', '', query, quote(remark)])
|
||||
|
||||
if coreProtocol == Protocol.Shadowsocks:
|
||||
proxyOutbound = None
|
||||
|
||||
for outbound in ob['outbounds']:
|
||||
if outbound['tag'] == 'proxy':
|
||||
proxyOutbound = outbound
|
||||
|
||||
break
|
||||
|
||||
if proxyOutbound is None:
|
||||
raise Exception('No proxy outbound found')
|
||||
|
||||
proxyServer = proxyOutbound['settings']['servers'][0]
|
||||
|
||||
method = proxyServer['method']
|
||||
password = proxyServer['password']
|
||||
address = proxyServer['address']
|
||||
port = proxyServer['port']
|
||||
|
||||
netloc = f'{quote(method)}:{quote(password)}@{address}:{port}'
|
||||
|
||||
return urlunparse(['ss', netloc, '', '', '', quote(remark)])
|
||||
else:
|
||||
raise UnsupportedServerExport('Unsupported core protocol export')
|
||||
|
||||
|
||||
class ProxyOutboundObject:
|
||||
class OutboundObject:
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def build(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ProxyOutboundObject(OutboundObject):
|
||||
def __init__(
|
||||
self,
|
||||
protocol,
|
||||
@@ -278,6 +310,8 @@ class ProxyOutboundObject:
|
||||
security,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.protocol = protocol
|
||||
self.remote_host = remote_host
|
||||
self.remote_port = remote_port
|
||||
@@ -496,6 +530,42 @@ class ProxyOutboundObject:
|
||||
return myJSON
|
||||
|
||||
|
||||
class ProxyOutboundObjectSS(OutboundObject):
|
||||
def __init__(self, method, password, address, port, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.method = method
|
||||
self.password = password
|
||||
self.address = address
|
||||
self.port = int(port)
|
||||
|
||||
def build(self):
|
||||
myJSON = {
|
||||
'tag': 'proxy',
|
||||
'protocol': 'shadowsocks',
|
||||
'settings': {
|
||||
'servers': [
|
||||
{
|
||||
'address': self.address,
|
||||
'port': self.port,
|
||||
'method': self.method,
|
||||
'password': self.password,
|
||||
'ota': False,
|
||||
},
|
||||
]
|
||||
},
|
||||
'streamSettings': {
|
||||
'network': 'tcp',
|
||||
},
|
||||
'mux': {
|
||||
'enabled': False,
|
||||
'concurrency': -1,
|
||||
},
|
||||
}
|
||||
|
||||
return myJSON
|
||||
|
||||
|
||||
class Outbounds:
|
||||
def __init__(self, proxyOutboundObject):
|
||||
self.proxyOutboundObject = proxyOutboundObject
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from Furious.Core.Core import XrayCore, Hysteria
|
||||
from Furious.Utility.Utility import protocolRepr
|
||||
from Furious.Utility.Utility import Protocol, protocolRepr
|
||||
|
||||
|
||||
class Intellisense:
|
||||
@@ -24,7 +24,7 @@ class Intellisense:
|
||||
return protocolRepr(outbound['protocol'])
|
||||
|
||||
if Intellisense.getCoreType(ob) == Hysteria.name():
|
||||
return 'hysteria'
|
||||
return Protocol.Hysteria
|
||||
|
||||
return ''
|
||||
except Exception:
|
||||
@@ -36,14 +36,25 @@ class Intellisense:
|
||||
def getCoreAddr(ob):
|
||||
try:
|
||||
if Intellisense.getCoreType(ob) == XrayCore.name():
|
||||
protocol = Intellisense.getCoreProtocol(ob)
|
||||
|
||||
for outbound in ob['outbounds']:
|
||||
if outbound['tag'] == 'proxy':
|
||||
return ';'.join(
|
||||
list(
|
||||
f'{server["address"]}'
|
||||
for server in outbound['settings']['vnext']
|
||||
if protocol == Protocol.VMess or protocol == Protocol.VLESS:
|
||||
return ';'.join(
|
||||
list(
|
||||
f'{server["address"]}'
|
||||
for server in outbound['settings']['vnext']
|
||||
)
|
||||
)
|
||||
|
||||
if protocol == Protocol.Shadowsocks:
|
||||
return ';'.join(
|
||||
list(
|
||||
f'{server["address"]}'
|
||||
for server in outbound['settings']['servers']
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if Intellisense.getCoreType(ob) == Hysteria.name():
|
||||
return ob['server'].split(':')[0]
|
||||
@@ -58,14 +69,25 @@ class Intellisense:
|
||||
def getCorePort(ob):
|
||||
try:
|
||||
if Intellisense.getCoreType(ob) == XrayCore.name():
|
||||
protocol = Intellisense.getCoreProtocol(ob)
|
||||
|
||||
for outbound in ob['outbounds']:
|
||||
if outbound['tag'] == 'proxy':
|
||||
return ';'.join(
|
||||
list(
|
||||
f'{server["port"]}'
|
||||
for server in outbound['settings']['vnext']
|
||||
if protocol == Protocol.VMess or protocol == Protocol.VLESS:
|
||||
return ';'.join(
|
||||
list(
|
||||
f'{server["port"]}'
|
||||
for server in outbound['settings']['vnext']
|
||||
)
|
||||
)
|
||||
|
||||
if protocol == Protocol.Shadowsocks:
|
||||
return ';'.join(
|
||||
list(
|
||||
f'{server["port"]}'
|
||||
for server in outbound['settings']['servers']
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if Intellisense.getCoreType(ob) == Hysteria.name():
|
||||
return ob['server'].split(':')[1]
|
||||
|
||||
@@ -21,7 +21,14 @@ class Base64Encoder:
|
||||
|
||||
@staticmethod
|
||||
def decode(text):
|
||||
return pybase64.b64decode(text, validate=True)
|
||||
return pybase64.b64decode(text, validate=False)
|
||||
|
||||
|
||||
class Protocol:
|
||||
VMess = 'VMess'
|
||||
VLESS = 'VLESS'
|
||||
Shadowsocks = 'shadowsocks'
|
||||
Hysteria = 'hysteria'
|
||||
|
||||
|
||||
class StateContext:
|
||||
@@ -148,12 +155,15 @@ bootstrapIconWhite = functools.partial(icon, 'bootstrap/white')
|
||||
|
||||
def protocolRepr(protocol):
|
||||
if protocol.lower() == 'vmess':
|
||||
return 'VMess'
|
||||
return Protocol.VMess
|
||||
|
||||
if protocol.lower() == 'vless':
|
||||
return 'VLESS'
|
||||
return Protocol.VLESS
|
||||
|
||||
return ''
|
||||
if protocol.lower() == 'shadowsocks':
|
||||
return Protocol.Shadowsocks
|
||||
|
||||
return protocol
|
||||
|
||||
|
||||
def getAbsolutePath(path):
|
||||
|
||||
Reference in New Issue
Block a user