Files
LorenEteval_Furious/Furious/Backends/Configuration.py
T
2026-08-12 12:20:47 +08:00

2406 lines
71 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (C) 2024present 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/>.
"""Parse, build, import, and export supported proxy configurations."""
from __future__ import annotations
from Furious.Frozenlib import *
from Furious.Interface import *
from Furious.Domain.Configuration import ConfigFactory
from Furious.Domain.Encoding import *
from typing import Union, Tuple
import copy
import functools
import urllib.parse
quote = functools.partial(urllib.parse.quote, safe='')
unquote = functools.partial(urllib.parse.unquote)
parse_qsl = functools.partial(urllib.parse.parse_qsl)
urlparse = functools.partial(urllib.parse.urlparse)
urlunparse = functools.partial(urllib.parse.urlunparse)
def queryStringFromItems(items):
"""Encode non-empty query items as a URI query string."""
return urllib.parse.urlencode(
list((key, value) for key, value in items if value != '' and value is not None),
doseq=True,
safe='%',
)
__all__ = [
'BLANK_CONFIG_XRAY',
'BLANK_CONFIG_HYSTERIA1',
'BLANK_CONFIG_HYSTERIA2',
'ConfigXray',
'ConfigHysteria1',
'ConfigHysteria2',
'configXrayEmptyProxyOutboundObject',
]
class ConfigXrayProxyOutboundObjectV(dict):
"""Represent and transform Xray proxy outbound object v configuration data."""
def __init__(
self,
protocol,
remote_host,
remote_port,
uuid_,
encryption,
type_,
security,
**kwargs,
):
"""Initialize the ConfigXrayProxyOutboundObjectV."""
(
networkObjectArgs,
securityArgs,
TLSObjectArgs,
finalmaskArgs,
) = (
{},
{},
{},
{},
)
if type_ == 'h2':
networkKey = 'httpSettings'
else:
networkKey = f'{type_}Settings'
networkObjectArgs[networkKey] = (
ConfigXray.kwargs2ProxyStreamSettingsNetworkObject(
type_, remote_host, kwargs
)
)
if security:
securityArgs['security'] = security
if security != 'none':
TLSObjectArgs[
# tlsSettings, realitySettings
f'{security}Settings'
] = ConfigXray.kwargs2ProxyStreamSettingsTLSObject(
protocol, remote_host, security, kwargs
)
try:
if kwargs.get('fm', ''):
finalmask = UJSONEncoder.decode(unquote(kwargs.get('fm', '')))
if finalmask:
finalmaskArgs['finalmask'] = finalmask
except Exception:
# Any non-exit exceptions
pass
super().__init__(
**{
'tag': 'proxy',
'protocol': protocol,
'settings': {
'vnext': [
{
'address': remote_host,
'port': int(remote_port),
'users': [
ConfigXray.kwargs2ProxyUserObject(
protocol, uuid_, encryption, kwargs
),
],
},
]
},
'streamSettings': {
'network': type_,
**networkObjectArgs,
**securityArgs,
**TLSObjectArgs,
**finalmaskArgs,
},
'mux': {
'enabled': False,
'concurrency': -1,
},
},
)
class ConfigXrayProxyOutboundObjectSS(dict):
"""Represent and transform Xray proxy outbound object ss configuration data."""
def __init__(self, method, password, address, port):
"""Initialize the ConfigXrayProxyOutboundObjectSS."""
super().__init__(
**{
'tag': 'proxy',
'protocol': 'shadowsocks',
'settings': {
'servers': [
{
'address': address,
'port': int(port),
'method': method,
'password': password,
'email': PROXY_OUTBOUND_USER_EMAIL,
'ota': False,
},
]
},
'streamSettings': {
'network': 'tcp',
},
'mux': {
'enabled': False,
'concurrency': -1,
},
}
)
class ConfigXrayProxyOutboundObjectSocks(dict):
"""Represent and transform Xray proxy outbound object SOCKS configuration data."""
def __init__(self, address, port, user='', password=''):
"""Initialize the ConfigXrayProxyOutboundObjectSocks."""
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):
"""Represent and transform Xray proxy outbound object trojan configuration data."""
def __init__(self, password, address, port, type_, security, **kwargs):
"""Initialize the ConfigXrayProxyOutboundObjectTrojan."""
networkObjectArgs, securityArgs, TLSObjectArgs = {}, {}, {}
if type_ == 'h2':
networkKey = 'httpSettings'
else:
networkKey = f'{type_}Settings'
networkObjectArgs[networkKey] = (
ConfigXray.kwargs2ProxyStreamSettingsNetworkObject(type_, address, kwargs)
)
if security:
securityArgs['security'] = security
if security != 'none':
TLSObjectArgs[
# tlsSettings, realitySettings
f'{security}Settings'
] = ConfigXray.kwargs2ProxyStreamSettingsTLSObject(
'trojan', address, security, kwargs
)
super().__init__(
**{
'tag': 'proxy',
'protocol': 'trojan',
'settings': {
'servers': [
{
'address': address,
'port': int(port),
'password': password,
'email': PROXY_OUTBOUND_USER_EMAIL,
},
]
},
'streamSettings': {
'network': type_,
**networkObjectArgs,
**securityArgs,
**TLSObjectArgs,
},
'mux': {
'enabled': False,
'concurrency': -1,
},
},
)
BLANK_CONFIG_XRAY = {
# log
'log': {
'access': '',
'error': '',
'loglevel': 'warning',
},
# inbounds
'inbounds': [
{
'tag': 'socks',
'port': 10808,
'listen': '127.0.0.1',
'protocol': 'socks',
'sniffing': {
'enabled': True,
'destOverride': [
'http',
'tls',
],
},
'settings': {
'auth': 'noauth',
'udp': True,
'allowTransparent': False,
},
},
{
'tag': 'http',
'port': 10809,
'listen': '127.0.0.1',
'protocol': 'http',
'sniffing': {
'enabled': True,
'destOverride': [
'http',
'tls',
],
},
'settings': {
'auth': 'noauth',
'udp': True,
'allowTransparent': False,
},
},
],
# outbounds
'outbounds': [
# proxy
{
'tag': 'proxy',
'protocol': '',
'settings': {},
'streamSettings': {
'network': 'tcp',
},
'mux': {
'enabled': False,
'concurrency': -1,
},
},
# direct
{
'tag': 'direct',
'protocol': 'freedom',
'settings': {},
},
# block
{
'tag': 'block',
'protocol': 'blackhole',
'settings': {
'response': {
'type': 'http',
}
},
},
],
# routing
'routing': {},
}
class ConfigXray(ConfigFactory):
"""Represent Xray configuration and supported share-link formats."""
def __init__(self, config: Union[str, dict] = '', **kwargs):
"""Initialize the ConfigXray."""
super().__init__(config, **kwargs)
def coreName(self) -> str:
"""Return the core implementation name."""
return 'Xray-core'
@staticmethod
def getProxyOutboundObject(config: dict, default=None) -> dict:
"""Return proxy outbound object."""
if not isinstance(config.get('outbounds'), list):
config['outbounds'] = []
for outbound in config['outbounds']:
if isinstance(outbound, dict):
tag = outbound.get('tag')
if isinstance(tag, str) and tag == 'proxy':
return outbound
if isinstance(default, dict):
# Proxy outbound not found. Add it
config['outbounds'].append(default)
return {}
@staticmethod
def getProxyOutboundStream(config: dict, **kwargs) -> dict:
"""Return proxy outbound stream."""
proxyOutbound = ConfigXray.getProxyOutboundObject(config, **kwargs)
if not isinstance(proxyOutbound.get('streamSettings'), dict):
proxyOutbound['streamSettings'] = {}
return proxyOutbound['streamSettings']
@staticmethod
def getProxyOutboundServer(config: dict, protocol: Protocol, **kwargs) -> dict:
"""Return proxy outbound server."""
value, proxyOutbound = (
protocol.value.casefold(),
ConfigXray.getProxyOutboundObject(config, **kwargs),
)
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:
server = {}
except Exception:
# Any non-exit exceptions
server = {}
if not isinstance(server, dict):
server = {}
return server
@staticmethod
def getProxyOutboundUser(config: dict, protocol: Protocol, **kwargs) -> dict:
"""Return proxy outbound user."""
proxyOutboundServer = ConfigXray.getProxyOutboundServer(
config, protocol, **kwargs
)
try:
user = proxyOutboundServer['users'][0]
except Exception:
# Any non-exit exceptions
user = {}
if not isinstance(user, dict):
user = {}
return user
@property
def proxyOutboundObject(self) -> dict:
"""Return the proxy outbound object value."""
try:
return ConfigXray.getProxyOutboundObject(self)
except Exception:
# Any non-exit exceptions
return {}
@property
def proxyProtocol(self) -> str:
"""Return the proxy protocol value."""
try:
return self.proxyOutboundObject['protocol']
except Exception:
# Any non-exit exceptions
return ''
@property
def proxyServerObject(self) -> dict:
"""Return the proxy server object value."""
try:
proxyProtocol = self.proxyProtocol.casefold()
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]
return {}
except Exception:
# Any non-exit exceptions
return {}
@property
def proxyUserObject(self) -> dict:
"""Return the proxy user object value."""
try:
if self.proxyProtocol.casefold() == 'socks':
return self.proxyServerObject
return self.proxyServerObject['users'][0]
except Exception:
# Any non-exit exceptions
return {}
@property
def proxyStreamSettingsObject(self) -> dict:
"""Return the proxy stream settings object value."""
try:
return ConfigXray.getProxyOutboundStream(self)
except Exception:
# Any non-exit exceptions
return {}
@property
def proxyStreamSettingsTLS(self) -> str:
"""Return the proxy stream settings TLS value."""
try:
return self.proxyStreamSettingsObject.get('security', 'none')
except Exception:
# Any non-exit exceptions
return ''
@property
def proxyStreamSettingsTLSObject(self) -> dict:
"""Return the proxy stream settings TLS object value."""
try:
# tlsSettings, realitySettings
TLSKey = f'{self.proxyStreamSettingsTLS}Settings'
return self.proxyStreamSettingsObject[TLSKey]
except Exception:
# Any non-exit exceptions
return {}
@property
def proxyStreamSettingsNetwork(self) -> str:
"""Return the proxy stream settings network value."""
try:
return self.proxyStreamSettingsObject['network']
except Exception:
# Any non-exit exceptions
return ''
@property
def proxyStreamSettingsNetworkObject(self) -> dict:
"""Return the proxy stream settings network object value."""
try:
if self.proxyStreamSettingsNetwork == 'h2':
networkKey = 'httpSettings'
else:
networkKey = f'{self.proxyStreamSettingsNetwork}Settings'
# Automatically use 'tcp' or 'raw' if needed
if (
networkKey == 'tcpSettings'
and networkKey not in self.proxyStreamSettingsObject
):
networkKey = 'rawSettings'
elif (
networkKey == 'rawSettings'
and networkKey not in self.proxyStreamSettingsObject
):
networkKey = 'tcpSettings'
return self.proxyStreamSettingsObject[networkKey]
except Exception:
# Any non-exit exceptions
return {}
@property
def kwargsFromVMessProxyStreamSettingsNetworkObject(self) -> dict:
"""Return the kwargs from v mess proxy stream settings network object value."""
kwargs = {}
try:
proxyStream = self.proxyStreamSettingsObject
if 'finalmask' in proxyStream:
kwargs['fm'] = quote(UJSONEncoder.encode(proxyStream['finalmask']))
except Exception:
# Any non-exit exceptions
pass
network = self.proxyStreamSettingsNetwork
networkObject = self.proxyStreamSettingsNetworkObject
if not networkObject:
return kwargs
def hasKey(key):
"""Return whether key."""
return networkObject.get(key) is not None
if network == 'tcp' or network == 'raw':
try:
kwargs['type'] = networkObject['header']['type']
except Exception:
# Any non-exit exceptions
pass
elif network == 'kcp':
try:
# Get order matters here
if hasKey('seed'):
kwargs['path'] = networkObject['seed']
kwargs['type'] = networkObject['header']['type']
except Exception:
# Any non-exit exceptions
pass
elif network == 'ws':
try:
# Get order matters here
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
if networkObject['headers']['Host']:
kwargs['host'] = quote(networkObject['headers']['Host'])
except Exception:
# Any non-exit exceptions
pass
elif network == 'h2' or network == 'http':
try:
# Get order matters here
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
kwargs['host'] = quote(','.join(networkObject['host']))
except Exception:
# Any non-exit exceptions
pass
elif network == 'quic':
try:
# Get order matters here
if hasKey('security'):
kwargs['host'] = networkObject['security']
if hasKey('key'):
kwargs['path'] = quote(networkObject['key'])
kwargs['type'] = networkObject['header']['type']
except Exception:
# Any non-exit exceptions
pass
elif network == 'grpc':
if hasKey('serviceName'):
kwargs['path'] = networkObject['serviceName']
elif network == 'httpupgrade':
try:
# Get order matters here
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
kwargs['host'] = quote(networkObject['host'])
except Exception:
# Any non-exit exceptions
pass
elif network == 'splithttp':
try:
# Get order matters here
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
kwargs['host'] = quote(networkObject['host'])
except Exception:
# Any non-exit exceptions
pass
elif network == 'xhttp':
try:
if hasKey('host'):
kwargs['host'] = quote(networkObject['host'])
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
if hasKey('mode'):
kwargs['mode'] = networkObject['mode']
if hasKey('extra'):
kwargs['extra'] = quote(UJSONEncoder.encode(networkObject['extra']))
except Exception:
# Any non-exit exceptions
pass
return kwargs
@property
def kwargsFromVLESSProxyStreamSettingsNetworkObject(self) -> dict:
"""Return the kwargs from VLESS proxy stream settings network object value."""
kwargs = {}
try:
proxyStream = self.proxyStreamSettingsObject
if 'finalmask' in proxyStream:
kwargs['fm'] = quote(UJSONEncoder.encode(proxyStream['finalmask']))
except Exception:
# Any non-exit exceptions
pass
network = self.proxyStreamSettingsNetwork
networkObject = self.proxyStreamSettingsNetworkObject
if not networkObject:
return kwargs
def hasKey(key):
"""Return whether key."""
return networkObject.get(key) is not None
if network == 'tcp' or network == 'raw':
try:
kwargs['headerType'] = networkObject['header']['type']
except Exception:
# Any non-exit exceptions
pass
elif network == 'kcp':
try:
# Get order matters here
if hasKey('seed'):
kwargs['seed'] = networkObject['seed']
kwargs['headerType'] = networkObject['header']['type']
except Exception:
# Any non-exit exceptions
pass
elif network == 'ws':
try:
# Get order matters here
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
if networkObject['headers']['Host']:
kwargs['host'] = quote(networkObject['headers']['Host'])
except Exception:
# Any non-exit exceptions
pass
elif network == 'h2' or network == 'http':
try:
# Get order matters here
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
kwargs['host'] = quote(','.join(networkObject['host']))
except Exception:
# Any non-exit exceptions
pass
elif network == 'quic':
try:
# Get order matters here
if hasKey('security'):
kwargs['quicSecurity'] = networkObject['security']
if hasKey('key'):
kwargs['path'] = quote(networkObject['key'])
kwargs['headerType'] = networkObject['header']['type']
except Exception:
# Any non-exit exceptions
pass
elif network == 'grpc':
if hasKey('serviceName'):
kwargs['serviceName'] = networkObject['serviceName']
if hasKey('multiMode'):
if networkObject['multiMode']:
kwargs['mode'] = 'multi'
if hasKey('authority'):
kwargs['authority'] = networkObject['authority']
elif network == 'httpupgrade':
try:
# Get order matters here
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
kwargs['host'] = quote(networkObject['host'])
except Exception:
# Any non-exit exceptions
pass
elif network == 'splithttp':
try:
# Get order matters here
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
kwargs['host'] = quote(networkObject['host'])
except Exception:
# Any non-exit exceptions
pass
elif network == 'xhttp':
try:
if hasKey('host'):
kwargs['host'] = quote(networkObject['host'])
if hasKey('path'):
kwargs['path'] = quote(networkObject['path'])
if hasKey('mode'):
kwargs['mode'] = networkObject['mode']
if hasKey('extra'):
kwargs['extra'] = quote(UJSONEncoder.encode(networkObject['extra']))
except Exception:
# Any non-exit exceptions
pass
return kwargs
@staticmethod
def kwargs2ProxyStreamSettingsNetworkObject(type_, remote_host, kwargs) -> dict:
# Note:
# v2rayN share standard doesn't require unquote. Still
# unquote value according to (VMess AEAD / VLESS) standard
"""Build Xray transport settings from share-link keyword arguments."""
if type_ == 'tcp' or type_ == 'raw':
TcpObject = {}
if kwargs.get('headerType', 'none'):
headerType = kwargs.get('headerType', 'none')
# Some dumb share link set this value to 'auto'. Protect it
if headerType != 'auto':
TcpObject['header'] = {
'type': headerType,
}
# Request settings for HTTP
if headerType == 'http' and kwargs.get('host'):
TcpObject['header']['request'] = {
'version': '1.1',
'method': 'GET',
'path': [kwargs.get('path', '/')],
'headers': {
'Host': [unquote(kwargs.get('host'))],
'User-Agent': [
(
'Mozilla/5.0 (Windows NT 10.0; WOW64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/53.0.2785.143 Safari/537.36'
),
(
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) '
'AppleWebKit/601.1 (KHTML, like Gecko) '
'CriOS/53.0.2785.109 '
'Mobile/14A456 '
'Safari/601.1.46'
),
],
'Accept-Encoding': ['gzip, deflate'],
'Connection': ['keep-alive'],
'Pragma': 'no-cache',
},
}
return TcpObject
elif type_ == 'kcp':
KcpObject = {
# Extension. From v2rayN
'uplinkCapacity': 12,
'downlinkCapacity': 100,
}
if kwargs.get('headerType', 'none'):
headerType = kwargs.get('headerType', 'none')
# Some dumb share link set this value to 'auto'. Protect it
if headerType != 'auto':
KcpObject['header'] = {
'type': headerType,
}
if kwargs.get('seed'):
KcpObject['seed'] = kwargs.get('seed')
return KcpObject
elif type_ == 'ws':
WebSocketObject = {}
if kwargs.get('path', '/'):
WebSocketObject['path'] = unquote(kwargs.get('path', '/'))
if kwargs.get('host'):
WebSocketObject['headers'] = {
'Host': unquote(kwargs.get('host')),
}
return WebSocketObject
elif type_ == 'h2' or type_ == 'http':
HttpObject = {}
if kwargs.get('host', remote_host):
# Protect "xxx," format
HttpObject['host'] = list(
filter(
lambda x: x != '',
unquote(kwargs.get('host', remote_host)).split(','),
)
)
if kwargs.get('path', '/'):
HttpObject['path'] = unquote(kwargs.get('path', '/'))
return HttpObject
elif type_ == 'quic':
QuicObject = {}
if kwargs.get('quicSecurity', 'none'):
QuicObject['security'] = kwargs.get('quicSecurity', 'none')
if kwargs.get('key'):
QuicObject['key'] = unquote(kwargs.get('key'))
if kwargs.get('headerType', 'none'):
headerType = kwargs.get('headerType', 'none')
# Some dumb share link set this value to 'auto'. Protect it
if headerType != 'auto':
QuicObject['header'] = {
'type': headerType,
}
return QuicObject
elif type_ == 'grpc':
GRPCObject = {}
if kwargs.get('serviceName'):
GRPCObject['serviceName'] = kwargs.get('serviceName')
if kwargs.get('mode', 'gun'):
GRPCObject['multiMode'] = kwargs.get('mode', 'gun') == 'multi'
if kwargs.get('authority'):
GRPCObject['authority'] = kwargs.get('authority')
return GRPCObject
elif type_ == 'httpupgrade':
httpUpgradeObject = {}
if kwargs.get('path', '/'):
httpUpgradeObject['path'] = unquote(kwargs.get('path', '/'))
if kwargs.get('host'):
httpUpgradeObject['host'] = unquote(kwargs.get('host'))
return httpUpgradeObject
elif type_ == 'splithttp':
splitHttpObject = {}
if kwargs.get('path', '/'):
splitHttpObject['path'] = unquote(kwargs.get('path', '/'))
if kwargs.get('host'):
splitHttpObject['host'] = unquote(kwargs.get('host'))
return splitHttpObject
elif type_ == 'xhttp':
xhttpObject = {}
if kwargs.get('host', ''):
xhttpObject['host'] = unquote(kwargs.get('host', ''))
if kwargs.get('path', '/'):
xhttpObject['path'] = unquote(kwargs.get('path', '/'))
if kwargs.get('mode', ''):
xhttpObject['mode'] = kwargs.get('mode', '')
try:
if kwargs.get('extra', ''):
xhttpObject['extra'] = UJSONEncoder.decode(
unquote(kwargs.get('extra', ''))
)
except Exception:
# Any non-exit exceptions
pass
return xhttpObject
else:
return {}
@property
def kwargsFromProxyStreamSettingsTLSObject(self) -> dict:
"""Return the kwargs from proxy stream settings TLS object value."""
kwargs = {}
TLS = self.proxyStreamSettingsTLS
TLSObject = self.proxyStreamSettingsTLSObject
if TLS == '' or TLS == 'none':
return kwargs
if TLS == 'reality' or TLS == 'tls':
if TLSObject.get('fingerprint'):
kwargs['fp'] = TLSObject['fingerprint']
if TLSObject.get('serverName'):
kwargs['sni'] = TLSObject['serverName']
if TLSObject.get('alpn'):
kwargs['alpn'] = quote(','.join(TLSObject['alpn']))
if TLSObject.get('echConfigList'):
kwargs['ech'] = quote(TLSObject['echConfigList'])
if TLSObject.get('pinnedPeerCertSha256'):
kwargs['pcs'] = quote(TLSObject['pinnedPeerCertSha256'])
if TLSObject.get('verifyPeerCertByName'):
kwargs['vcn'] = quote(TLSObject['verifyPeerCertByName'])
if TLS == 'reality':
# More kwargs for reality
if TLSObject.get('publicKey'):
kwargs['pbk'] = TLSObject['publicKey']
if TLSObject.get('shortId'):
kwargs['sid'] = TLSObject['shortId']
if TLSObject.get('mldsa65Verify'):
kwargs['pqv'] = TLSObject['mldsa65Verify']
if TLSObject.get('spiderX'):
kwargs['spx'] = quote(TLSObject['spiderX'])
return kwargs
@staticmethod
def kwargs2ProxyStreamSettingsTLSObject(
protocol, remote_host, security, kwargs
) -> dict:
"""Build Xray TLS or REALITY settings from share-link keyword arguments."""
TLSObject = {}
if security == 'reality' or security == 'tls':
# Note: If specify default value 'chrome', some share link fails.
# Leave default value as empty
fp = kwargs.get('fp')
sni = kwargs.get('sni')
# Protect "xxx," format
alpn = list(
filter(
lambda x: x != '',
unquote(kwargs.get('alpn', '')).split(','),
)
)
ech = unquote(kwargs.get('ech', ''))
pcs = unquote(kwargs.get('pcs', ''))
vcn = unquote(kwargs.get('vcn', ''))
if fp:
TLSObject['fingerprint'] = fp
if sni:
TLSObject['serverName'] = sni
else:
host = ''
if protocol == 'vmess':
host = kwargs.get('host')
if protocol == 'vless':
host = remote_host
if host:
TLSObject['serverName'] = host
if alpn:
TLSObject['alpn'] = alpn
if ech:
TLSObject['echConfigList'] = ech
if pcs:
TLSObject['pinnedPeerCertSha256'] = pcs
if vcn:
TLSObject['verifyPeerCertByName'] = vcn
if security == 'reality':
# More args for reality
pbk = kwargs.get('pbk')
sid = kwargs.get('sid', '')
pqv = kwargs.get('pqv', '')
spx = kwargs.get('spx', '')
if pbk:
TLSObject['publicKey'] = pbk
TLSObject['shortId'] = sid
TLSObject['mldsa65Verify'] = pqv
TLSObject['spiderX'] = unquote(spx)
return TLSObject
@staticmethod
def kwargs2ProxyUserObject(protocol, uuid_, encryption, kwargs) -> dict:
"""Build an Xray proxy user object from share-link values."""
if protocol == 'vmess':
UserObject = {
'id': uuid_,
'security': encryption,
'email': PROXY_OUTBOUND_USER_EMAIL,
}
# For VMess(v2rayN share standard) only.
if kwargs.get('aid') is not None:
# int: extra guard
UserObject['alterId'] = int(kwargs.get('aid'))
return UserObject
if protocol == 'vless':
UserObject = {
'id': uuid_,
'encryption': encryption,
'email': PROXY_OUTBOUND_USER_EMAIL,
}
if kwargs.get('flow'):
# flow is empty, TLS. Otherwise, XTLS.
UserObject['flow'] = kwargs.get('flow')
return UserObject
return {}
@staticmethod
def URI2ProxyOutboundObjectVMess(URI: str) -> Tuple[str, dict]:
"""Parse a VMess URI into a remark and Xray proxy outbound."""
try:
myHead, myBody = URI.split('://')
except Exception:
# Any non-exit exceptions
return '', {}
try:
myData = PyBase64Encoder.decode(myBody)
myJSON = UJSONEncoder.decode(myData)
except Exception:
# Any non-exit exceptions
return ConfigXray.URI2ProxyOutboundObjectVLESS(URI, protocol='vmess')
else:
def getOrDefault(key, default=''):
"""Return or default."""
return myJSON.get(key, default)
remark = unquote(getOrDefault('ps'))
return (
remark,
# Ignore: v
ConfigXrayProxyOutboundObjectV(
'vmess',
getOrDefault('add'),
int(getOrDefault('port', '0')),
getOrDefault('id'),
getOrDefault('scy', 'auto'),
getOrDefault('net', 'tcp'),
getOrDefault('tls', 'none'),
# kwargs. v2rayN share standard -> (VMess AEAD / VLESS) standard
aid=int(getOrDefault('aid', '0')),
headerType=getOrDefault('type', 'none'),
host=getOrDefault('host'),
quicSecurity=getOrDefault('host', 'none'),
path=getOrDefault('path'),
key=getOrDefault('path'),
seed=getOrDefault('path'),
serviceName=getOrDefault('path'),
sni=getOrDefault('sni'),
alpn=getOrDefault('alpn'),
# Note: If specify default value 'chrome', some share link fails.
# Leave default value as empty
fp=getOrDefault('fp'),
),
)
@staticmethod
def URI2ProxyOutboundObjectVLESS(URI: str, **kwargs) -> Tuple[str, dict]:
"""Parse a VLESS URI into a remark and Xray proxy outbound."""
result = urlparse(URI)
remark = unquote(result.fragment)
queryObject = {key: value for key, value in parse_qsl(result.query)}
uuid_, server = result.netloc.split('@')
remote_host, remote_port = parseHostPort(server)
encryption = queryObject.pop('encryption', 'none')
type_ = queryObject.pop('type', 'tcp')
security = queryObject.pop('security', 'none')
return (
remark,
ConfigXrayProxyOutboundObjectV(
kwargs.pop('protocol', 'vless'),
remote_host,
int(remote_port),
uuid_,
encryption,
type_,
security,
# kwargs
**queryObject,
),
)
@staticmethod
def URI2ProxyOutboundObjectSS(URI: str) -> Tuple[str, dict]:
"""Parse a Shadowsocks URI into a remark and Xray proxy outbound."""
try:
result = urlparse(URI)
remark = unquote(result.fragment)
except Exception:
# Any non-exit exceptions
return '', {}
def getSSParams():
# Begin SIP002...
"""Return ss params."""
try:
# Try pack with 3 element
userinfo, server = result.netloc.split('@')
# Some old SS share link doesn't add padding
# in base64 encoding. Add padding to userinfo
return [
*PyBase64Encoder.decode(userinfo + '===').decode().split(':', 1),
*parseHostPort(server),
]
except Exception:
# Any non-exit exceptions
pass
try:
# Try pack with 4 element
userinfo, server = result.netloc.split('@')
return [*userinfo.split(':', 1), *parseHostPort(server)]
except Exception:
# Any non-exit exceptions
pass
try:
# ss://base64...#fragment
userinfo, server = (
PyBase64Encoder.decode(result.netloc).decode().split('@')
)
return [*userinfo.split(':', 1), *parseHostPort(server)]
except Exception:
# Any non-exit exceptions
pass
raise ValueError(f'Invalid SS URI format {URI}')
return (
remark,
ConfigXrayProxyOutboundObjectSS(*getSSParams()),
)
@staticmethod
def URI2ProxyOutboundObjectSocks(URI: str) -> Tuple[str, dict]:
"""Parse a SOCKS URI into a remark and Xray proxy outbound."""
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]:
"""Parse a Trojan URI into a remark and Xray proxy outbound."""
result = urlparse(URI)
remark = unquote(result.fragment)
queryObject = {key: value for key, value in parse_qsl(result.query)}
password, server = result.netloc.split('@')
address, port = parseHostPort(server)
type_ = queryObject.pop('type', 'tcp')
# For Trojan: Assign tls by default
security = queryObject.pop('security', 'tls')
return (
remark,
ConfigXrayProxyOutboundObjectTrojan(
password, address, port, type_, security, **queryObject
),
)
@staticmethod
def URI2ProxyOutboundObject(URI: str) -> Tuple[str, dict]:
"""Dispatch a supported share URI to its Xray outbound parser."""
if URI.startswith('vmess://'):
return ConfigXray.URI2ProxyOutboundObjectVMess(URI)
if URI.startswith('vless://'):
return ConfigXray.URI2ProxyOutboundObjectVLESS(URI)
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)
raise ValueError(f'Unrecognized URI scheme {URI}')
@property
def itemProtocol(self) -> str:
"""Return the item protocol value."""
return Protocol.toEnum(self.proxyProtocol).value
@property
def itemAddress(self) -> str:
"""Return the item address value."""
addr = self.proxyServerObject.get('address', '')
return str(addr)
@property
def itemPort(self) -> str:
"""Return the item port value."""
port = self.proxyServerObject.get('port', '')
return str(port)
@property
def itemTransport(self) -> str:
"""Return the item transport value."""
return self.proxyStreamSettingsNetwork
@property
def itemTLS(self) -> str:
"""Return the item TLS value."""
return self.proxyStreamSettingsTLS
@property
def itemLatency(self) -> str:
# Backward compatibility
"""Return the item latency value."""
return self.getExtras('delayResult')
@property
def itemSpeed(self) -> str:
# Backward compatibility
"""Return the item speed value."""
return self.getExtras('speedResult')
def toJSONString(self, **kwargs) -> str:
"""Serialize the configuration as JSON text."""
indent = kwargs.pop('indent', 2)
return super().toJSONString(indent=indent)
def toURI(self, remark: str = '') -> str:
"""Export the configuration as a share URI."""
if remark == '':
override = self.itemRemark
else:
override = remark
protocol = self.proxyProtocol.casefold()
if protocol == 'vmess':
netloc = PyBase64Encoder.encode(
UJSONEncoder.encode(
{
'v': '2',
'ps': override,
**{
key: str(self.proxyServerObject[value])
for key, value in {
'add': 'address',
'port': 'port',
}.items()
},
**{
key: str(self.proxyUserObject[value])
for key, value in {
'id': 'id',
'aid': 'alterId',
'scy': 'security',
}.items()
},
'net': self.proxyStreamSettingsNetwork,
'tls': self.proxyStreamSettingsTLS,
# kwargs
**self.kwargsFromVMessProxyStreamSettingsNetworkObject,
**self.kwargsFromProxyStreamSettingsTLSObject,
}
).encode()
).decode()
return urlunparse(['vmess', netloc, '', '', {}, ''])
if protocol == 'vless':
flowArg = {}
if self.proxyUserObject.get('flow'):
flowArg['flow'] = self.proxyUserObject['flow']
netloc = (
self.proxyUserObject['id']
+ '@'
+ self.proxyServerObject['address']
+ ':'
+ str(self.proxyServerObject['port'])
)
query = queryStringFromItems(
{
'encryption': self.proxyUserObject['encryption'],
'type': self.proxyStreamSettingsNetwork,
'security': self.proxyStreamSettingsTLS,
# kwargs
**flowArg,
**self.kwargsFromVLESSProxyStreamSettingsNetworkObject,
**self.kwargsFromProxyStreamSettingsTLSObject,
}.items()
)
return urlunparse(['vless', netloc, '', '', query, quote(override)])
if protocol == 'shadowsocks':
method, password, address, port = list(
self.proxyServerObject[value]
for value in ['method', 'password', 'address', 'port']
)
netloc = f'{quote(method)}:{quote(password)}@{address}:{port}'
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]
for value in ['password', 'address', 'port']
)
netloc = f'{quote(password)}@{address}:{port}'
query = queryStringFromItems(
{
'type': self.proxyStreamSettingsNetwork,
'security': self.proxyStreamSettingsTLS,
# kwargs
**self.kwargsFromVLESSProxyStreamSettingsNetworkObject,
**self.kwargsFromProxyStreamSettingsTLSObject,
}.items()
)
return urlunparse(['trojan', netloc, '', '', query, quote(override)])
# Unrecognized protocol
return ''
def fromURI(self, URI: str) -> bool:
"""Populate the configuration from a share URI."""
try:
remark, proxyOutboundObject = ConfigXray.URI2ProxyOutboundObject(URI)
factory = copy.deepcopy(BLANK_CONFIG_XRAY)
factory['outbounds'] = [
# proxy
proxyOutboundObject,
# direct
{
'tag': 'direct',
'protocol': 'freedom',
'settings': {},
},
# block
{
'tag': 'block',
'protocol': 'blackhole',
'settings': {
'response': {
'type': 'http',
}
},
},
]
dict.__init__(self, **factory)
self.setExtras('remark', remark)
return True
except Exception:
# Any non-exit exceptions
return False
def httpProxy(self) -> str:
"""Return the configured local HTTP proxy endpoint."""
try:
for inbound in self['inbounds']:
if inbound['protocol'] == 'http':
# Note: If there are multiple http inbounds
# satisfied, the first one will be chosen.
return str(inbound['listen']) + ':' + str(inbound['port'])
return ''
except Exception:
# Any non-exit exceptions
return ''
def socksProxy(self) -> str:
"""Return the configured local SOCKS proxy endpoint."""
try:
for inbound in self['inbounds']:
if inbound['protocol'] == 'socks':
# Note: If there are multiple socks inbounds
# satisfied, the first one will be chosen.
return str(inbound['listen']) + ':' + str(inbound['port'])
return ''
except Exception:
# Any non-exit exceptions
return ''
def setHttpProxy(self, endpoint: str) -> bool:
"""Set the local HTTP proxy endpoint."""
try:
if self.get('inbounds') is None:
self['inbounds'] = []
if not isinstance(self['inbounds'], list):
self['inbounds'] = []
if endpoint == '':
for inbound in self['inbounds']:
if inbound['protocol'] == 'http':
self['inbounds'].remove(inbound)
# Remove first chosen
return True
# None satisfied. Return success
return True
listen, port = parseHostPort(endpoint)
for inbound in self['inbounds']:
if inbound['protocol'] == 'http':
# Note: If there are multiple http inbounds
# satisfied, the first one will be chosen.
inbound['listen'], inbound['port'] = listen, int(port)
return True
# No entry. Add new one
self['inbounds'].append(
{
'tag': 'http',
'port': int(port),
'listen': listen,
'protocol': 'http',
'sniffing': {
'enabled': True,
'destOverride': [
'http',
'tls',
],
},
'settings': {
'auth': 'noauth',
'udp': True,
'allowTransparent': False,
},
}
)
return True
except Exception:
# Any non-exit exceptions
return False
def setSocksProxy(self, endpoint: str) -> bool:
"""Set the local SOCKS proxy endpoint."""
try:
if self.get('inbounds') is None:
self['inbounds'] = []
if not isinstance(self['inbounds'], list):
self['inbounds'] = []
if endpoint == '':
for inbound in self['inbounds']:
if inbound['protocol'] == 'socks':
self['inbounds'].remove(inbound)
# Remove first chosen
return True
# None satisfied. Return success
return True
listen, port = parseHostPort(endpoint)
for inbound in self['inbounds']:
if inbound['protocol'] == 'socks':
# Note: If there are multiple socks inbounds
# satisfied, the first one will be chosen.
inbound['listen'], inbound['port'] = listen, int(port)
return True
# No entry. Add new one
self['inbounds'].append(
{
'tag': 'socks',
'port': int(port),
'listen': listen,
'protocol': 'socks',
'sniffing': {
'enabled': True,
'destOverride': [
'http',
'tls',
],
},
'settings': {
'auth': 'noauth',
'udp': True,
'allowTransparent': False,
},
}
)
return True
except Exception:
# Any non-exit exceptions
return False
BLANK_CONFIG_HYSTERIA1 = {
'server': '',
'protocol': 'udp',
'socks5': {
'listen': '127.0.0.1:10808',
},
'http': {
'listen': '127.0.0.1:10809',
},
}
class ConfigHysteria1(ConfigFactory):
"""Represent Hysteria 1 client configuration and share links."""
def __init__(self, config: Union[str, dict] = '', **kwargs):
"""Initialize the ConfigHysteria1."""
super().__init__(config, **kwargs)
def coreName(self) -> str:
"""Return the core implementation name."""
return 'Hysteria1'
@property
def itemProtocol(self) -> str:
"""Return the item protocol value."""
return Protocol.Hysteria1.value
@property
def itemAddress(self) -> str:
"""Return the item address value."""
server = self.get('server', '')
pos = server.rfind(':')
if pos == -1:
return server
else:
return server[:pos]
@property
def itemPort(self) -> str:
"""Return the item port value."""
server = self.get('server', '')
pos = server.rfind(':')
if pos == -1:
return ''
else:
return server[pos + 1 :]
@property
def itemTransport(self) -> str:
"""Return the item transport value."""
return ''
@property
def itemTLS(self) -> str:
"""Return the item TLS value."""
return ''
@property
def itemLatency(self) -> str:
"""Return the item latency value."""
return self.getExtras('delayResult')
@property
def itemSpeed(self) -> str:
"""Return the item speed value."""
return self.getExtras('speedResult')
def toJSONString(self, **kwargs) -> str:
"""Serialize the configuration as JSON text."""
indent = kwargs.pop('indent', 4)
return super().toJSONString(indent=indent)
def toURI(self, remark: str = '') -> str:
"""Export the configuration as a share URI."""
if remark == '':
override = self.itemRemark
else:
override = remark
try:
netloc, mport = self['server'].split(',')
except Exception:
# Any non-exit exceptions
netloc, mport = self['server'], ''
if mport:
mportArgs = {'mport': mport}
else:
mportArgs = {}
protocol = self.get('protocol', 'udp')
if self.get('auth_str'):
# auth in string
authArgs = {'auth': self['auth_str']}
else:
authArgs = {}
if self.get('server_name'):
peerArgs = {'peer': self['server_name']}
else:
peerArgs = {}
if self.get('insecure') is True:
insecureArgs = {'insecure': '1'}
else:
insecureArgs = {}
upmbps = self['up_mbps']
downmbps = self['down_mbps']
if self.get('alpn'):
alpnArgs = {'alpn': self['alpn']}
else:
alpnArgs = {}
if self.get('obfs'):
obfsParamArgs = {'obfsParam': self['obfs']}
else:
obfsParamArgs = {}
query = queryStringFromItems(
{
**mportArgs,
'protocol': protocol,
**authArgs,
**peerArgs,
**insecureArgs,
'upmbps': upmbps,
'downmbps': downmbps,
**alpnArgs,
**obfsParamArgs,
}.items()
)
return urlunparse(['hysteria', netloc, '', '', query, quote(override)])
def fromURI(self, URI: str) -> bool:
"""Populate the configuration from a share URI."""
try:
result = urlparse(URI)
remark = unquote(result.fragment)
queryObject = {key: value for key, value in parse_qsl(result.query)}
if result.scheme != 'hysteria':
raise ValueError('Invalid hysteria1 URI scheme')
server = result.netloc
mport = queryObject.get('mport', '')
if mport:
server += f',{mport}'
protocol = queryObject.get('protocol', 'udp')
if queryObject.get('auth', ''):
# auth in string
authArgs = {'auth_str': queryObject['auth']}
else:
authArgs = {}
if queryObject.get('peer', ''):
peerArgs = {'server_name': queryObject['peer']}
else:
peerArgs = {}
insecure = queryObject.get('insecure', False)
if isinstance(insecure, bool):
pass
elif insecure == '1':
insecure = True
else:
insecure = False
upmbps = queryObject.get('upmbps', 24)
downmbps = queryObject.get('downmbps', 96)
if queryObject.get('alpn', ''):
alpnArgs = {'alpn': queryObject['alpn']}
else:
alpnArgs = {}
# obfs mode seems not required, ignored...
if queryObject.get('obfsParam', ''):
obfsParamArgs = {'obfs': queryObject['obfsParam']}
else:
obfsParamArgs = {}
dict.__init__(
self,
**{
'server': server,
'protocol': protocol,
**authArgs,
**obfsParamArgs,
'up_mbps': int(upmbps),
'down_mbps': int(downmbps),
**peerArgs,
**alpnArgs,
'insecure': insecure,
'socks5': {
'listen': '127.0.0.1:10808',
},
'http': {
'listen': '127.0.0.1:10809',
},
},
)
self.setExtras('remark', remark)
return True
except Exception:
# Any non-exit exceptions
return False
def httpProxy(self) -> str:
"""Return the configured local HTTP proxy endpoint."""
try:
return self['http']['listen']
except Exception:
# Any non-exit exceptions
return ''
def socksProxy(self) -> str:
"""Return the configured local SOCKS proxy endpoint."""
try:
return self['socks5']['listen']
except Exception:
# Any non-exit exceptions
return ''
def setHttpProxy(self, endpoint: str) -> bool:
"""Set the local HTTP proxy endpoint."""
try:
if endpoint == '':
self.pop('http', None)
return True
if self.get('http') is None:
self['http'] = {}
self['http']['listen'] = endpoint
return True
except Exception:
# Any non-exit exceptions
return False
def setSocksProxy(self, endpoint: str) -> bool:
"""Set the local SOCKS proxy endpoint."""
try:
if endpoint == '':
self.pop('socks5', None)
return True
if self.get('socks5') is None:
self['socks5'] = {}
self['socks5']['listen'] = endpoint
return True
except Exception:
# Any non-exit exceptions
return False
BLANK_CONFIG_HYSTERIA2 = {
'server': '',
'tls': {
'insecure': False,
},
'socks5': {
'listen': '127.0.0.1:10808',
},
'http': {
'listen': '127.0.0.1:10809',
},
}
class ConfigHysteria2(ConfigFactory):
"""Represent Hysteria 2 client configuration and share links."""
def __init__(self, config: Union[str, dict] = '', **kwargs):
"""Initialize the ConfigHysteria2."""
super().__init__(config, **kwargs)
def coreName(self) -> str:
"""Return the core implementation name."""
return 'Hysteria2'
@property
def itemProtocol(self) -> str:
"""Return the item protocol value."""
return Protocol.Hysteria2.value
@staticmethod
def splitServerAddressPort(server: str) -> Tuple[str, str]:
"""Split a standard or Realm-mode server value into display fields."""
if server.startswith(('realm://', 'realm+http://')):
result = urlparse(server)
try:
port = result.port
except ValueError:
port = None
return result.hostname or '', str(port) if port is not None else ''
pos = server.rfind(':')
if pos == -1:
return server, ''
else:
return server[:pos], server[pos + 1 :]
@property
def itemAddress(self) -> str:
"""Return the item address value."""
address, _port = self.splitServerAddressPort(self.get('server', ''))
return address
@property
def itemPort(self) -> str:
"""Return the item port value."""
_address, port = self.splitServerAddressPort(self.get('server', ''))
return port
@property
def itemTransport(self) -> str:
"""Return the item transport value."""
return ''
@property
def itemTLS(self) -> str:
"""Return the item TLS value."""
return ''
@property
def itemLatency(self) -> str:
"""Return the item latency value."""
return self.getExtras('delayResult')
@property
def itemSpeed(self) -> str:
"""Return the item speed value."""
return self.getExtras('speedResult')
def toJSONString(self, **kwargs) -> str:
"""Serialize the configuration as JSON text."""
indent = kwargs.pop('indent', 4)
return super().toJSONString(indent=indent)
@staticmethod
def splitHysteria2RealmQueryItems(queryItems):
"""Separate Realm transport parameters from Hysteria 2 parameters."""
realmItems = []
hysteria2Items = []
for key, value in queryItems:
if key in ['stun', 'lport']:
realmItems.append((key, value))
else:
hysteria2Items.append((key, value))
return realmItems, hysteria2Items
def toURI(self, remark: str = '') -> str:
"""Export the configuration as a share URI."""
if remark == '':
override = self.itemRemark
else:
override = remark
TLSArg, obfsArg = {}, {}
if self.get('tls'):
if self['tls'].get('sni'):
TLSArg['sni'] = self['tls']['sni']
if self['tls'].get('insecure') is True:
TLSArg['insecure'] = '1'
else:
TLSArg['insecure'] = '0'
if self['tls'].get('pinSHA256'):
TLSArg['pinSHA256'] = self['tls']['pinSHA256']
if self.get('obfs'):
obfsType = self['obfs'].get('type', 'salamander')
obfsArg['obfs'] = obfsType
obfsArg['obfs-password'] = self['obfs'][obfsType]['password']
if self['server'].startswith(('realm://', 'realm+http://')):
result = urlparse(self['server'])
if result.scheme == 'realm+http':
scheme = 'hysteria2+realm+http'
else:
scheme = 'hysteria2+realm'
realmItems, _hysteria2Items = self.splitHysteria2RealmQueryItems(
parse_qsl(result.query)
)
query = queryStringFromItems(
[
('auth', self['auth']),
*TLSArg.items(),
*obfsArg.items(),
*realmItems,
]
)
return urlunparse(
[scheme, result.netloc, result.path, '', query, quote(override)]
)
netloc, query = (
self['auth'] + '@' + self['server'],
queryStringFromItems([*TLSArg.items(), *obfsArg.items()]),
)
return urlunparse(['hysteria2', netloc, '', '', query, quote(override)])
def fromURI(self, URI: str) -> bool:
"""Populate the configuration from a share URI."""
try:
result = urlparse(URI)
remark = unquote(result.fragment)
queryItems = parse_qsl(result.query)
queryObject = {key: value for key, value in queryItems}
if result.scheme not in [
'hysteria2',
'hy2',
'hysteria2+realm',
'hysteria2+realm+http',
]:
raise ValueError('Invalid hysteria2 URI scheme')
if result.scheme in ['hysteria2+realm', 'hysteria2+realm+http']:
auth = queryObject.get('auth', '')
realmItems, _hysteria2Items = self.splitHysteria2RealmQueryItems(
queryItems
)
if result.scheme == 'hysteria2+realm+http':
realmScheme = 'realm+http'
else:
realmScheme = 'realm'
server = urlunparse(
[
realmScheme,
result.netloc,
result.path,
'',
queryStringFromItems(realmItems),
'',
]
)
else:
auth, server = result.netloc.split('@')
obfs = queryObject.get('obfs', '')
obfsPassword = queryObject.get('obfs-password', '')
sni = queryObject.get('sni', '')
insecure = queryObject.get('insecure', False)
if isinstance(insecure, bool):
pass
elif insecure == '1':
insecure = True
else:
insecure = False
pinSHA256 = queryObject.get('pinSHA256', '')
obfsArg = {}
pinSHA256Arg = {}
if obfs and obfsPassword:
obfsArg['obfs'] = {
'type': obfs,
obfs: {
'password': obfsPassword,
},
}
if pinSHA256:
pinSHA256Arg['pinSHA256'] = pinSHA256
dict.__init__(
self,
**{
'server': server,
'auth': auth,
**obfsArg,
'tls': {
'sni': sni,
'insecure': insecure,
**pinSHA256Arg,
},
'socks5': {
'listen': '127.0.0.1:10808',
},
'http': {
'listen': '127.0.0.1:10809',
},
},
)
self.setExtras('remark', remark)
return True
except Exception:
# Any non-exit exceptions
return False
def httpProxy(self) -> str:
"""Return the configured local HTTP proxy endpoint."""
try:
return self['http']['listen']
except Exception:
# Any non-exit exceptions
return ''
def socksProxy(self) -> str:
"""Return the configured local SOCKS proxy endpoint."""
try:
return self['socks5']['listen']
except Exception:
# Any non-exit exceptions
return ''
def setHttpProxy(self, endpoint: str) -> bool:
"""Set the local HTTP proxy endpoint."""
try:
if endpoint == '':
self.pop('http', None)
return True
if self.get('http') is None:
self['http'] = {}
self['http']['listen'] = endpoint
return True
except Exception:
# Any non-exit exceptions
return False
def setSocksProxy(self, endpoint: str) -> bool:
"""Set the local SOCKS proxy endpoint."""
try:
if endpoint == '':
self.pop('socks5', None)
return True
if self.get('socks5') is None:
self['socks5'] = {}
self['socks5']['listen'] = endpoint
return True
except Exception:
# Any non-exit exceptions
return False
def configXrayEmptyProxyOutboundObject(protocol: Protocol) -> dict:
"""Return the config Xray empty proxy outbound object value used by the application."""
value = protocol.value.casefold()
if value == 'vless' or value == 'vmess':
return {
'tag': 'proxy',
'protocol': value,
'settings': {
'vnext': [
{
'address': '',
'port': 0,
'users': [
{
'email': PROXY_OUTBOUND_USER_EMAIL,
},
],
},
]
},
'streamSettings': {
'network': 'tcp',
},
'mux': {
'enabled': False,
'concurrency': -1,
},
}
elif value == 'shadowsocks':
return {
'tag': 'proxy',
'protocol': 'shadowsocks',
'settings': {
'servers': [
{
'address': '',
'port': 0,
'method': '',
'password': '',
'email': PROXY_OUTBOUND_USER_EMAIL,
'ota': False,
}
]
},
'streamSettings': {
'network': 'tcp',
},
'mux': {
'enabled': False,
'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',
'protocol': 'trojan',
'settings': {
'servers': [
{
'address': '',
'port': 0,
'password': '',
'email': PROXY_OUTBOUND_USER_EMAIL,
}
]
},
'streamSettings': {
'network': 'tcp',
},
'mux': {
'enabled': False,
'concurrency': -1,
},
}
else:
return {}