mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
2548 lines
76 KiB
Python
2548 lines
76 KiB
Python
# Copyright (C) 2024–present 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.Library.Encoder 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',
|
||
'configFactoryFromDict',
|
||
'configFactoryFromAny',
|
||
'configFactoryBlank',
|
||
]
|
||
|
||
|
||
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.lower(),
|
||
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.lower()
|
||
|
||
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.lower() == '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.lower()
|
||
|
||
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.lower()
|
||
|
||
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 {}
|
||
|
||
|
||
def configFactoryFromDict(config: dict, **kwargs) -> ConfigFactory:
|
||
"""Return the config factory from dict value used by the application."""
|
||
if not isinstance(config, dict):
|
||
return ConfigFactory()
|
||
|
||
def hasField(field):
|
||
"""Return whether field."""
|
||
return config.get(field) is not None
|
||
|
||
if hasField('inbounds') or hasField('outbounds'):
|
||
# Assuming is Xray-Core
|
||
return ConfigXray(config, **kwargs)
|
||
|
||
if hasField('server'):
|
||
if (
|
||
hasField('protocol')
|
||
or hasField('up_mbps')
|
||
or hasField('down_mbps')
|
||
or hasField('auth_str')
|
||
or hasField('alpn')
|
||
or hasField('server_name')
|
||
or hasField('insecure')
|
||
or hasField('recv_window_conn')
|
||
or hasField('recv_window')
|
||
or isinstance(config.get('obfs'), str)
|
||
or hasField('fast_open')
|
||
or hasField('lazy_start')
|
||
):
|
||
return ConfigHysteria1(config, **kwargs)
|
||
if (
|
||
hasField('auth')
|
||
or hasField('tls')
|
||
or hasField('transport')
|
||
or hasField('quic')
|
||
or hasField('bandwidth')
|
||
or hasField('tcpForwarding')
|
||
or hasField('udpForwarding')
|
||
or hasField('tcpTProxy')
|
||
or hasField('udpTProxy')
|
||
or isinstance(config.get('obfs'), dict)
|
||
or hasField('fastOpen')
|
||
or hasField('lazy')
|
||
):
|
||
return ConfigHysteria2(config, **kwargs)
|
||
|
||
# Copied to factory unrecognized
|
||
return ConfigFactory(config, **kwargs)
|
||
|
||
|
||
def configFactoryFromAny(config: Union[str, dict], **kwargs) -> ConfigFactory:
|
||
"""Return the config factory from any value used by the application."""
|
||
if isinstance(config, str):
|
||
if config.startswith(
|
||
(
|
||
'vmess://',
|
||
'vless://',
|
||
'ss://',
|
||
'trojan://',
|
||
'socks://',
|
||
'socks5://',
|
||
'socks5h://',
|
||
)
|
||
):
|
||
return ConfigXray(config, **kwargs)
|
||
if config.startswith('hysteria://'):
|
||
return ConfigHysteria1(config, **kwargs)
|
||
if config.startswith(
|
||
(
|
||
'hy2://',
|
||
'hysteria2://',
|
||
'hysteria2+realm://',
|
||
'hysteria2+realm+http://',
|
||
)
|
||
):
|
||
return ConfigHysteria2(config, **kwargs)
|
||
|
||
try:
|
||
# Try to construct from JSON string
|
||
return configFactoryFromDict(UJSONEncoder.decode(config), **kwargs)
|
||
except Exception:
|
||
# Any non-exit exceptions
|
||
|
||
return ConfigFactory(**kwargs)
|
||
|
||
if isinstance(config, dict):
|
||
return configFactoryFromDict(config, **kwargs)
|
||
|
||
return ConfigFactory(**kwargs)
|
||
|
||
|
||
def configFactoryBlank(protocol: Protocol) -> ConfigFactory:
|
||
"""Return the config factory blank value used by the application."""
|
||
if protocol == Protocol.VMess or protocol == Protocol.VLESS:
|
||
factory = configFactoryFromDict(BLANK_CONFIG_XRAY)
|
||
factory['outbounds'][0]['protocol'] = protocol.value.lower()
|
||
factory['outbounds'][0]['settings']['vnext'] = [
|
||
{
|
||
'address': '',
|
||
'port': 0,
|
||
'users': [{'email': PROXY_OUTBOUND_USER_EMAIL}],
|
||
},
|
||
]
|
||
|
||
return factory
|
||
|
||
if protocol == Protocol.Socks:
|
||
factory = configFactoryFromDict(BLANK_CONFIG_XRAY)
|
||
factory['outbounds'][0]['protocol'] = protocol.value.lower()
|
||
factory['outbounds'][0]['settings'] = {
|
||
'address': '',
|
||
'port': 0,
|
||
}
|
||
|
||
return factory
|
||
|
||
if protocol == Protocol.Shadowsocks or protocol == Protocol.Trojan:
|
||
factory = configFactoryFromDict(BLANK_CONFIG_XRAY)
|
||
factory['outbounds'][0]['protocol'] = protocol.value.lower()
|
||
factory['outbounds'][0]['settings']['servers'] = [
|
||
{
|
||
'address': '',
|
||
'port': 0,
|
||
'email': PROXY_OUTBOUND_USER_EMAIL,
|
||
},
|
||
]
|
||
|
||
return factory
|
||
|
||
if protocol == Protocol.Hysteria1:
|
||
factory = configFactoryFromDict(BLANK_CONFIG_HYSTERIA1)
|
||
|
||
return factory
|
||
if protocol == Protocol.Hysteria2:
|
||
factory = configFactoryFromDict(BLANK_CONFIG_HYSTERIA2)
|
||
|
||
return factory
|
||
|
||
return ConfigFactory()
|