Add support for shadowsocks import/export

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2023-08-05 11:32:28 +08:00
parent b2419f232b
commit dceed1d24a
4 changed files with 218 additions and 34 deletions
+85 -3
View File
@@ -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
+85 -15
View File
@@ -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
+34 -12
View File
@@ -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]
+14 -4
View File
@@ -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):