diff --git a/Furious/Action/Import.py b/Furious/Action/Import.py index f7fbebd..a204024 100644 --- a/Furious/Action/Import.py +++ b/Furious/Action/Import.py @@ -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 diff --git a/Furious/Core/Configuration.py b/Furious/Core/Configuration.py index 8a436de..020259d 100644 --- a/Furious/Core/Configuration.py +++ b/Furious/Core/Configuration.py @@ -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 diff --git a/Furious/Core/Intellisense.py b/Furious/Core/Intellisense.py index 847cea3..e3be6c9 100644 --- a/Furious/Core/Intellisense.py +++ b/Furious/Core/Intellisense.py @@ -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] diff --git a/Furious/Utility/Utility.py b/Furious/Utility/Utility.py index 365b359..9a50a88 100644 --- a/Furious/Utility/Utility.py +++ b/Furious/Utility/Utility.py @@ -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):