From 4a0618aff25825d4044e5285aee83454bc5c2eb7 Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Sun, 22 Oct 2023 19:28:52 +0800 Subject: [PATCH] Add hysteria2 share link import/export support Signed-off-by: Loren Eteval --- Furious/Action/Import.py | 54 ++++++++++++++++++++++ Furious/Core/Configuration.py | 87 ++++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/Furious/Action/Import.py b/Furious/Action/Import.py index b513634..328cda2 100644 --- a/Furious/Action/Import.py +++ b/Furious/Action/Import.py @@ -21,6 +21,7 @@ from Furious.Core.Configuration import ( ProxyOutboundObject, ProxyOutboundObjectSS, ProxyOutboundObjectTrojan, + Hysteria2Configuration, ) from Furious.Gui.Action import Action from Furious.Widget.Widget import Menu, MessageBox @@ -476,6 +477,53 @@ class ImportLinkAction(Action): return '', False + @staticmethod + def parseShareLinkHysteria2(data, importServerArgs=None): + if importServerArgs is None: + importServerArgs = {} + + try: + parseResult = urllib.parse.urlparse(data) + + queryObject = { + key: value for key, value in urllib.parse.parse_qsl(parseResult.query) + } + + remark = urllib.parse.unquote(parseResult.fragment) + + if remark == '': + remark = _('Untitled') + + auth, server = parseResult.netloc.split('@') + + myJSON = Hysteria2Configuration( + server, + auth, + # kwargs + **queryObject, + ).build() + + APP().ServerWidget.importServer( + remark, + ujson.dumps( + myJSON, indent=4, ensure_ascii=False, escape_forward_slashes=False + ), + **importServerArgs, + ) + + logger.debug( + f'import share link success. ' + f'Remark: {remark}. Protocol: {Protocol.Hysteria2}' + ) + + return remark, True + except Exception as ex: + # Any non-exit exceptions + + logger.error(f'import share link failed: {data}. Exception: {ex}') + + return '', False + @staticmethod def parseShareLink(shareLink, importServerArgs=None): if importServerArgs is None: @@ -526,6 +574,12 @@ class ImportLinkAction(Action): importServerArgs, ) + if myHead.lower() == 'hysteria2' or myHead.lower() == 'hy2': + return ImportLinkAction.parseShareLinkHysteria2( + shareLink, + importServerArgs, + ) + logger.error(f'unsupported share link: {shareLink}') return '', False diff --git a/Furious/Core/Configuration.py b/Furious/Core/Configuration.py index b3d32dc..00a3808 100644 --- a/Furious/Core/Configuration.py +++ b/Furious/Core/Configuration.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from Furious.Core.Core import XrayCore +from Furious.Core.Core import XrayCore, Hysteria2 from Furious.Core.Intellisense import Intellisense from Furious.Widget.Widget import MessageBox from Furious.Utility.Constants import APPLICATION_NAME, PROXY_OUTBOUND_USER_EMAIL @@ -387,6 +387,39 @@ class Configuration: return exportFuncMap[Intellisense.getCoreProtocol(ob)]() + if Intellisense.getCoreType(ob) == Hysteria2.name(): + netloc = f'{ob["auth"]}@{ob["server"]}' + + tlsArg, obfsArg = {}, {} + + if ob.get('tls'): + if ob['tls'].get('sni'): + tlsArg['sni'] = ob['tls']['sni'] + + if ob['tls'].get('insecure') is True: + tlsArg['insecure'] = '1' + else: + tlsArg['insecure'] = '0' + + if ob['tls'].get('pinSHA256'): + tlsArg['pinSHA256'] = ob['tls']['pinSHA256'] + + if ob.get('obfs'): + obfsType = ob['obfs'].get('type', 'salamander') + + obfsArg['obfs'] = obfsType + obfsArg['obfs-password'] = ob['obfs'][obfsType]['password'] + + query = '&'.join( + f'{key}={value}' + for key, value in { + **tlsArg, + **obfsArg, + }.items() + ) + + return urlunparse(['hysteria2', netloc, '', '', query, quote(remark)]) + raise UnsupportedServerExport('Unsupported core protocol export') @@ -828,3 +861,55 @@ class XrayCoreConfiguration: myJSON['routing'] = {} return myJSON + + +class Hysteria2Configuration: + def __init__(self, server, auth, **kwargs): + self.server = server + self.auth = auth + self.obfs = kwargs.get('obfs', '') + self.obfsPassword = kwargs.get('obfs-password', '') + self.sni = kwargs.get('sni', '') + + insecure = kwargs.get('insecure', False) + + if isinstance(insecure, bool): + self.insecure = insecure + elif insecure == '1': + self.insecure = True + else: + self.insecure = False + + self.pinSHA256 = kwargs.get('pinSHA256', '') + + def build(self): + obfsArg = {} + pinSHA256Arg = {} + + if self.obfs and self.obfsPassword: + obfsArg['obfs'] = { + 'type': self.obfs, + self.obfs: { + 'password': self.obfsPassword, + }, + } + + if self.pinSHA256: + pinSHA256Arg['pinSHA256'] = self.pinSHA256 + + return { + 'server': self.server, + 'auth': self.auth, + 'tls': { + 'sni': self.sni, + 'insecure': self.insecure, + **pinSHA256Arg, + }, + **obfsArg, + 'socks5': { + 'listen': '127.0.0.1:10808', + }, + 'http': { + 'listen': '127.0.0.1:10809', + }, + }