Browse Source

Merge branch 'dev'

master
dnomd343 2 years ago
parent
commit
51675b1698
  1. 59
      Basis/Api.py
  2. 18
      Basis/Check.py
  3. 11
      Basis/Compile.py
  4. 72
      Basis/Constant.py
  5. 8
      Basis/DnsProxy.py
  6. 26
      Basis/Exception.py
  7. 30
      Basis/Filter.py
  8. 4
      Basis/Functions.py
  9. 26
      Basis/Logger.py
  10. 35
      Basis/Manager.py
  11. 23
      Basis/Process.py
  12. 159
      Basis/Test.py
  13. 26
      Builder/Brook.py
  14. 6
      Builder/Hysteria.py
  15. 51
      Builder/Shadowsocks.py
  16. 7
      Builder/ShadowsocksR.py
  17. 2
      Builder/Trojan.py
  18. 7
      Builder/V2ray.py
  19. 5
      Builder/VLESS.py
  20. 3
      Builder/VMess.py
  21. 7
      Builder/Xray.py
  22. 14
      Builder/__init__.py
  23. 23
      Dockerfile
  24. 10
      Filter/__init__.py
  25. 5
      Tester/Brook.py
  26. 5
      Tester/Hysteria.py
  27. 10
      Tester/Plugin.py
  28. 14
      Tester/Settings.py
  29. 89
      Tester/Shadowsocks.py
  30. 5
      Tester/ShadowsocksR.py
  31. 5
      Tester/Trojan.py
  32. 14
      Tester/TrojanGo.py
  33. 2
      Tester/V2ray.py
  34. 5
      Tester/VLESS.py
  35. 5
      Tester/VMess.py
  36. 2
      Tester/Xray.py
  37. 128
      Tester/__init__.py
  38. 8
      env.yaml
  39. 135
      main.py
  40. 66
      test.py

59
Basis/Api.py

@ -1,16 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import json import json
from gevent import pywsgi from gevent import pywsgi
from Checker import formatCheck from Checker import formatCheck
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Manager import Manager from Basis.Manager import Manager
from Basis.Constant import Version
from flask import Flask, Response, request from flask import Flask, Response, request
from Basis.Exception import managerException
from Basis.Constant import ApiPort, ApiPath, ApiToken, Version
token = ''
webPath = '/' # root of api server
webApi = Flask(__name__) # init flask server webApi = Flask(__name__) # init flask server
@ -43,16 +43,16 @@ def genError(message: str) -> Response:
def tokenCheck() -> bool: def tokenCheck() -> bool:
if token == '': return True # without token check if ApiToken == '': return True # without token check
if request.method == 'GET': if request.method == 'GET':
return request.args.get('token') == token return request.args.get('token') == ApiToken
elif request.method == 'POST': elif request.method == 'POST':
return request.json.get('token') == token return request.json.get('token') == ApiToken
else: else:
return False # polyfill return False # polyfill
@webApi.route('/task', methods = ['GET']) @webApi.route(os.path.join(ApiPath, 'task'), methods = ['GET'])
def getTaskList() -> Response: def getTaskList() -> Response:
if not tokenCheck(): # token check if not tokenCheck(): # token check
return genError('Invalid token') return genError('Invalid token')
@ -64,17 +64,22 @@ def getTaskList() -> Response:
}) })
@webApi.route('/task', methods = ['POST']) @webApi.route(os.path.join(ApiPath, 'task'), methods = ['POST'])
def createTask() -> Response: def createTask() -> Response:
if not tokenCheck(): # token check if not tokenCheck(): # token check
return genError('Invalid token') return genError('Invalid token')
# TODO: format check and proxy list try:
checkList = formatCheck(request.json.get('check')) # TODO: format check and proxy list
checkList = formatCheck(request.json.get('check'))
except:
return genError('Some error in check options')
proxyList = [] proxyList = []
for proxy in request.json.get('proxy'): for proxy in request.json.get('proxy'):
proxyList.append(formatProxy(proxy)) try:
logging.critical(proxyList) proxyList.append(formatProxy(proxy))
except Exception as exp:
return genError('Proxy error in %s -> %s' % (proxy, exp))
logging.debug('API create task -> check = %s | proxy = %s' % (checkList, proxyList)) logging.debug('API create task -> check = %s | proxy = %s' % (checkList, proxyList))
tasks = [] tasks = []
@ -93,7 +98,7 @@ def createTask() -> Response:
}) })
@webApi.route('/task/<taskId>', methods = ['GET']) @webApi.route(os.path.join(ApiPath, 'task/<taskId>'), methods = ['GET'])
def getTaskInfo(taskId: str) -> Response: def getTaskInfo(taskId: str) -> Response:
if not tokenCheck(): # token check if not tokenCheck(): # token check
return genError('Invalid token') return genError('Invalid token')
@ -106,7 +111,23 @@ def getTaskInfo(taskId: str) -> Response:
}) })
@webApi.route('/version', methods = ['GET']) @webApi.route(os.path.join(ApiPath, 'task/<taskId>'), methods = ['DELETE'])
def deleteTask(taskId: str) -> Response:
if not tokenCheck(): # token check
return genError('Invalid token')
logging.debug('API get task -> %s' % taskId)
if not Manager.isUnion(taskId):
return genError('Task not found')
try:
Manager.delUnion(taskId)
return jsonResponse({
'success': True
})
except managerException as exp:
return genError(str(exp))
@webApi.route(os.path.join(ApiPath, 'version'), methods = ['GET'])
def getVersion() -> Response: def getVersion() -> Response:
logging.debug('API get version -> %s' + Version) logging.debug('API get version -> %s' + Version)
return jsonResponse({ return jsonResponse({
@ -115,9 +136,7 @@ def getVersion() -> Response:
}) })
def startServer(apiToken: str = '', apiPort: int = 7839) -> None: def startServer() -> None:
global token logging.warning('API server at http://:%i%s' % (ApiPort, ApiPath))
token = apiToken # api token (default empty) logging.warning('API ' + ('without token' if ApiToken == '' else 'token -> %s' % ApiToken))
logging.warning('API server at http://:%i/' % apiPort) pywsgi.WSGIServer(('0.0.0.0', ApiPort), webApi).serve_forever() # powered by gevent
logging.warning('API ' + ('without token' if apiToken == '' else 'token -> %s' % apiToken))
pywsgi.WSGIServer(('0.0.0.0', apiPort), webApi).serve_forever() # powered by gevent

18
Basis/Check.py

@ -6,6 +6,8 @@ import time
from Checker import Checker from Checker import Checker
from Basis.Logger import logging from Basis.Logger import logging
from Builder import Builder, clientEntry from Builder import Builder, clientEntry
from Basis.Exception import checkException
from Basis.Functions import checkPortStatus
def buildClient(taskId: str, taskInfo: dict) -> Builder: def buildClient(taskId: str, taskInfo: dict) -> Builder:
@ -18,24 +20,27 @@ def buildClient(taskId: str, taskInfo: dict) -> Builder:
) )
except Exception as reason: except Exception as reason:
logging.error('[%s] Client build error -> %s' % (taskId, reason)) logging.error('[%s] Client build error -> %s' % (taskId, reason))
raise RuntimeError('Client build error') raise checkException('Client build error')
def waitClient(taskId: str, client: Builder): def waitClient(taskId: str, client: Builder, times: int = 150, delay: int = 100): # wait until client port occupied
# TODO: wait port occupied (client.socksPort) for i in range(times):
time.sleep(1) # TODO: simple delay for now if not checkPortStatus(client.socksPort): # port occupied
break
time.sleep(delay / 1000) # wait in default: 100ms * 150 => 15s
time.sleep(1) # wait a short time before check process
if not client.status(): # client unexpected exit if not client.status(): # client unexpected exit
logging.warning('[%s] Client unexpected exit' % taskId) logging.warning('[%s] Client unexpected exit' % taskId)
client.destroy() # remove file and kill sub process client.destroy() # remove file and kill sub process
logging.debug('[%s] Client output\n%s', (taskId, client.output)) logging.debug('[%s] Client output\n%s', (taskId, client.output))
raise RuntimeError('Client unexpected exit') raise checkException('Client unexpected exit')
def Check(taskId: str, taskInfo: dict) -> dict: def Check(taskId: str, taskInfo: dict) -> dict:
logging.info('[%s] Start checking process -> %s' % (taskId, taskInfo)) logging.info('[%s] Start checking process -> %s' % (taskId, taskInfo))
if taskInfo['type'] not in clientEntry: # unknown proxy type if taskInfo['type'] not in clientEntry: # unknown proxy type
logging.error('[%s] Unknown proxy type %s' % (taskId, taskInfo['type'])) logging.error('[%s] Unknown proxy type %s' % (taskId, taskInfo['type']))
raise RuntimeError('Unknown proxy type') raise checkException('Unknown proxy type')
client = buildClient(taskId, taskInfo) # build proxy client client = buildClient(taskId, taskInfo) # build proxy client
logging.info('[%s] Client loaded successfully' % taskId) logging.info('[%s] Client loaded successfully' % taskId)
waitClient(taskId, client) # wait for the client to start waitClient(taskId, client) # wait for the client to start
@ -49,5 +54,6 @@ def Check(taskId: str, taskInfo: dict) -> dict:
taskInfo.pop('check') # remove check items taskInfo.pop('check') # remove check items
return { return {
**taskInfo, **taskInfo,
'success': True,
'result': checkResult, # add check result 'result': checkResult, # add check result
} }

11
Basis/Compile.py

@ -1,11 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import compileall
from Basis.Logger import logging
def startCompile(dirRange: str = '/') -> None:
for optimize in [-1, 1, 2]:
compileall.compile_dir(dirRange, quiet = 1, maxlevels = 256, optimize = optimize)
logging.warning('Python optimize compile -> %s (level = %i)' % (dirRange, optimize))

72
Basis/Constant.py

@ -1,11 +1,70 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import yaml
# Global Options
Version = 'dev' Version = 'dev'
ApiPath = '/'
ApiPort = 7839
ApiToken = ''
CheckThread = 64
LogLevel = 'INFO'
LogFile = 'runtime.log'
DnsServer = None
WorkDir = '/tmp/ProxyC' WorkDir = '/tmp/ProxyC'
TestHost = 'proxyc.net'
TestSite = 'www.bing.com'
PathEnv = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin' PathEnv = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin'
# Load Env Options
envOptions = {}
try:
yamlFile = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../env.yaml')
yamlContent = open(yamlFile, 'r', encoding = 'utf-8').read()
envOptions = yaml.load(yamlContent, Loader = yaml.FullLoader)
except: # something error in env.yaml
pass
if 'version' in envOptions:
Version = envOptions['version']
if 'loglevel' in envOptions:
LogLevel = envOptions['loglevel']
if 'dir' in envOptions:
WorkDir = envOptions['dir']
if 'dns' in envOptions:
DnsServer = envOptions['dns']
if 'api' in envOptions:
if 'port' in envOptions['api']:
ApiPort = envOptions['api']['port']
if 'path' in envOptions['api']:
ApiPath = envOptions['api']['path']
if 'token' in envOptions['api']:
ApiToken = envOptions['api']['token']
# WorkDir Create
try:
os.makedirs(WorkDir) # just like `mkdir -p ...`
except:
pass # folder exist or target is another thing
# Shadowsocks Info # Shadowsocks Info
mbedtlsMethods = [
'aes-128-cfb128',
'aes-192-cfb128',
'aes-256-cfb128',
'camellia-128-cfb128',
'camellia-192-cfb128',
'camellia-256-cfb128',
]
ssMethods = { # methods support of different Shadowsocks project ssMethods = { # methods support of different Shadowsocks project
'ss-rust': [ # table method removed refer to https://github.com/shadowsocks/shadowsocks-rust/issues/887 'ss-rust': [ # table method removed refer to https://github.com/shadowsocks/shadowsocks-rust/issues/887
'none', 'plain', 'rc4', 'rc4-md5', 'none', 'plain', 'rc4', 'rc4-md5',
@ -70,6 +129,7 @@ ssAllMethods = set()
[ssAllMethods.update(ssMethods[x]) for x in ssMethods] [ssAllMethods.update(ssMethods[x]) for x in ssMethods]
ssAllMethods = sorted(list(ssAllMethods)) # methods of Shadowsocks ssAllMethods = sorted(list(ssAllMethods)) # methods of Shadowsocks
# Plugin Info # Plugin Info
Plugins = { Plugins = {
'simple-obfs': ['obfs-local', 'obfs-server'], 'simple-obfs': ['obfs-local', 'obfs-server'],
@ -90,6 +150,7 @@ Plugins = {x: [Plugins[x][0], Plugins[x][1 if len(Plugins[x]) == 2 else 0]] for
Plugins = {x: {'client': Plugins[x][0], 'server': Plugins[x][1]} for x in Plugins} # format plugins info Plugins = {x: {'client': Plugins[x][0], 'server': Plugins[x][1]} for x in Plugins} # format plugins info
pluginClients = [Plugins[x]['client'] for x in Plugins] # plugin client list -> obfs-local / simple-tls / ... pluginClients = [Plugins[x]['client'] for x in Plugins] # plugin client list -> obfs-local / simple-tls / ...
# ShadowsocksR Info # ShadowsocksR Info
ssrMethods = [ # methods of ShadowsocksR ssrMethods = [ # methods of ShadowsocksR
'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr',
@ -118,19 +179,20 @@ ssrObfuscations = [ # obfuscations of ShadowsocksR (obfs)
'tls_simple', 'tls1.2_ticket_auth', 'tls1.2_ticket_fastauth', 'tls_simple', 'tls1.2_ticket_auth', 'tls1.2_ticket_fastauth',
] ]
# VMess Info
# V2ray / Xray Info
vmessMethods = ['aes-128-gcm', 'chacha20-poly1305', 'auto', 'none', 'zero'] vmessMethods = ['aes-128-gcm', 'chacha20-poly1305', 'auto', 'none', 'zero']
# XTLS Info quicMethods = ['none', 'aes-128-gcm', 'chacha20-poly1305']
udpObfuscations = ['none', 'srtp', 'utp', 'wechat-video', 'dtls', 'wireguard']
xtlsFlows = ['xtls-origin', 'xtls-direct', 'xtls-splice'] xtlsFlows = ['xtls-origin', 'xtls-direct', 'xtls-splice']
xtlsFlows = {x: x.replace('-', '-rprx-') for x in xtlsFlows} xtlsFlows = {x: x.replace('-', '-rprx-') for x in xtlsFlows}
# v2ray / Xray Info
quicMethods = ['none', 'aes-128-gcm', 'chacha20-poly1305']
udpObfuscations = ['none', 'srtp', 'utp', 'wechat-video', 'dtls', 'wireguard']
# Trojan-Go Info # Trojan-Go Info
trojanGoMethods = ['aes-128-gcm', 'aes-256-gcm', 'chacha20-ietf-poly1305'] trojanGoMethods = ['aes-128-gcm', 'aes-256-gcm', 'chacha20-ietf-poly1305']
# Hysteria Info # Hysteria Info
hysteriaProtocols = ['udp', 'wechat-video', 'faketcp'] hysteriaProtocols = ['udp', 'wechat-video', 'faketcp']

8
Basis/DnsProxy.py

@ -15,12 +15,16 @@ def daemon(process: subprocess.Popen, command: list, gap: int = 2) -> None: # d
while True: # start daemon while True: # start daemon
time.sleep(gap) # check time gap time.sleep(gap) # check time gap
if process.poll() is not None: # unexpected exit if process.poll() is not None: # unexpected exit
logging.warning('dnsproxy unexpected exit') logging.error('DnsProxy unexpected exit\n%s\n%s%s' % (
logging.debug('output of dnsproxy\n%s' % process.stdout.read().decode('utf-8')) '-' * 96, process.stdout.read().decode('utf-8'), '-' * 96)
)
process = run(command) process = run(command)
def start(servers: list or None, port: int = 53, cache: int = 4194304) -> None: # default cache size -> 4MiB def start(servers: list or None, port: int = 53, cache: int = 4194304) -> None: # default cache size -> 4MiB
if servers is not None and type(servers) != list: # invalid server content
logging.error('Invalid DNS server -> %s' % servers)
return
if servers is None or len(servers) == 0: # use origin dns server if servers is None or len(servers) == 0: # use origin dns server
logging.info('Skip dnsproxy process') logging.info('Skip dnsproxy process')
return return

26
Basis/Exception.py

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class buildException(Exception): # for build error
def __init__(self, reason):
self.reason = reason
class filterException(Exception): # for filter error
def __init__(self, reason):
self.reason = reason
class processException(Exception): # for process error
def __init__(self, reason):
self.reason = reason
class managerException(Exception): # for manager error
def __init__(self, reason):
self.reason = reason
class checkException(Exception): # for check error
def __init__(self, reason):
self.reason = reason

30
Basis/Filter.py

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import copy import copy
from Basis.Exception import filterException
filterObject = { filterObject = {
'optional': { 'optional': {
@ -67,7 +68,7 @@ for field in filterObject:
def Filter(raw: dict, rules: dict) -> dict: def Filter(raw: dict, rules: dict) -> dict:
if type(raw) != dict: if type(raw) != dict:
raise RuntimeError('Invalid input for filter') raise filterException('Invalid input for filter')
data = {} data = {}
raw = copy.deepcopy(raw) raw = copy.deepcopy(raw)
rules = copy.deepcopy(rules) rules = copy.deepcopy(rules)
@ -75,19 +76,19 @@ def Filter(raw: dict, rules: dict) -> dict:
# pretreatment process (raw --[copy / default value]--> data) # pretreatment process (raw --[copy / default value]--> data)
if key not in raw: # key not exist if key not in raw: # key not exist
if not rule['optional']: # force require key not exist if not rule['optional']: # force require key not exist
raise RuntimeError('Missing `%s` field' % key) raise filterException('Missing `%s` field' % key)
data[key] = rule['default'] # set default value data[key] = rule['default'] # set default value
else: # key exist else: # key exist
data[key] = raw[key] data[key] = raw[key]
# format process (data --[format]--> data) # format process (data --[format]--> data)
if data[key] is None: # key content is None if data[key] is None: # key content is None
if not rule['allowNone']: # key is not allow None if not rule['allowNone']: # key is not allow None
raise RuntimeError('Field `%s` shouldn\'t be None' % key) raise filterException('Field `%s` shouldn\'t be None' % key)
continue # skip following process continue # skip following process
try: try:
data[key] = rule['format'](data[key]) # run format data[key] = rule['format'](data[key]) # run format
except: except:
raise RuntimeError(rule['errMsg']) # format error raise filterException(rule['errMsg']) # format error
# filter process (data --[type check (& filter check)]--> pass / non-pass) # filter process (data --[type check (& filter check)]--> pass / non-pass)
if type(rule['type']) == type: # str / int / bool / ... if type(rule['type']) == type: # str / int / bool / ...
rule['type'] = [rule['type']] # str -> [str] / int -> [int] / ... rule['type'] = [rule['type']] # str -> [str] / int -> [int] / ...
@ -95,35 +96,38 @@ def Filter(raw: dict, rules: dict) -> dict:
if data[key] == any and any in rule['type']: # special case -> skip type filter if data[key] == any and any in rule['type']: # special case -> skip type filter
pass pass
elif type(data[key]) not in rule['type']: # type not in allow list elif type(data[key]) not in rule['type']: # type not in allow list
raise RuntimeError('Invalid `%s` field' % key) raise filterException('Invalid `%s` field' % key)
elif type(rule['type']) == dict: # check subObject elif type(rule['type']) == dict: # check subObject
if type(data[key]) != dict: if type(data[key]) != dict:
raise RuntimeError('Invalid sub object in `%s`' % key) # subObject content should be dict raise filterException('Invalid sub object in `%s`' % key) # subObject content should be dict
if not rule['multiSub']: # single subObject if not rule['multiSub']: # single subObject
subRules = rule['type'] subRules = rule['type']
else: # multi subObject else: # multi subObject
if rule['indexKey'] not in data[key]: # confirm index key exist if rule['indexKey'] not in data[key]: # confirm index key exist
raise RuntimeError('Index key `%s` not found in `%s`' % (rule['indexKey'], key)) raise filterException('Index key `%s` not found in `%s`' % (rule['indexKey'], key))
subType = data[key][rule['indexKey']].lower() subType = data[key][rule['indexKey']].lower()
if subType not in rule['type']: # confirm subObject rule exist if subType not in rule['type']: # confirm subObject rule exist
raise RuntimeError('Unknown index `%s` in key `%s`' % (subType, key)) raise filterException('Unknown index `%s` in key `%s`' % (subType, key))
subRules = rule['type'][subType] subRules = rule['type'][subType]
try: try:
data[key] = Filter(data[key], subRules) data[key] = Filter(data[key], subRules)
except RuntimeError as exp: except filterException as exp:
raise RuntimeError('%s (in `%s`)' % (exp, key)) # add located info raise filterException('%s (in `%s`)' % (exp, key)) # add located info
continue continue
elif rule['type'] != any: # type == any -> skip type filter elif rule['type'] != any: # type == any -> skip type filter
raise RuntimeError('Unknown `type` in rules') raise filterException('Unknown `type` in rules')
if not rule['filter'](data[key]): # run filter if not rule['filter'](data[key]): # run filter
raise RuntimeError(rule['errMsg']) raise filterException(rule['errMsg'])
return data return data
def rulesFilter(rules: dict) -> dict: def rulesFilter(rules: dict) -> dict:
result = {} result = {}
for key, rule in rules.items(): # filter by basic rules for key, rule in rules.items(): # filter by basic rules
result[key] = Filter(rule, filterObject) try:
result[key] = Filter(rule, filterObject)
except filterException as exp:
raise filterException('%s (`%s` in rules)' % (exp, key)) # rules error
return result return result

4
Basis/Functions.py

@ -55,6 +55,10 @@ def hostFormat(host: str, v6Bracket: bool = False) -> str:
return host return host
def v6AddBracket(host: str) -> str: # add bracket for ipv6
return hostFormat(host, v6Bracket = True)
def genFlag(length: int = 12) -> str: # generate random task flag def genFlag(length: int = 12) -> str: # generate random task flag
flag = '' flag = ''
for i in range(0, length): for i in range(0, length):

26
Basis/Logger.py

@ -4,17 +4,23 @@
import sys import sys
import logging import logging
from colorlog import ColoredFormatter from colorlog import ColoredFormatter
from Basis.Constant import LogLevel, LogFile
logLevel = { # log level
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
'critical': logging.CRITICAL
}[LogLevel.lower()]
dateFormat = '%Y-%m-%d %H:%M:%S' # log date format
logFormat = '[%(asctime)s] [%(levelname)s] %(message)s (%(module)s.%(funcName)s)' # log format
logFile = 'runtime.log'
logLevel = logging.DEBUG
# logLevel = logging.WARNING
dateFormat = '%Y-%m-%d %H:%M:%S'
logFormat = '[%(asctime)s] [%(levelname)s] %(message)s (%(module)s.%(funcName)s)'
logging.basicConfig( logging.basicConfig(
level = logLevel, level = logLevel,
format = logFormat, format = logFormat,
datefmt = dateFormat, datefmt = dateFormat,
filename = logFile, filename = LogFile,
) )
logHandler = logging.StreamHandler(stream = sys.stdout) logHandler = logging.StreamHandler(stream = sys.stdout)
logHandler.setFormatter(ColoredFormatter( logHandler.setFormatter(ColoredFormatter(
@ -29,11 +35,3 @@ logHandler.setFormatter(ColoredFormatter(
} }
)) ))
logging.getLogger().addHandler(logHandler) logging.getLogger().addHandler(logHandler)
if __name__ == '__main__':
logging.debug('debug')
logging.info('info')
logging.warning('warn')
logging.error('error')
logging.critical('critical')

35
Basis/Manager.py

@ -4,7 +4,7 @@
import copy import copy
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Functions import genFlag from Basis.Functions import genFlag
from Basis.Exception import managerException
class Task(object): class Task(object):
""" Manage global check task. """ Manage global check task.
@ -39,15 +39,26 @@ class Task(object):
logging.info('Manager add task [%s] -> %s' % (taskId, task)) logging.info('Manager add task [%s] -> %s' % (taskId, task))
self.__tasks.update(tasks) # load into task list self.__tasks.update(tasks) # load into task list
self.__unions[unionId] = { self.__unions[unionId] = {
'finish': False,
'items': taskIds # record task items 'items': taskIds # record task items
} }
logging.info('Manager add union [%s] -> %s' % (unionId, taskIds)) logging.info('Manager add union [%s] -> %s' % (unionId, taskIds))
return unionId return unionId
def delUnion(self, unionId) -> None: # remove union
if unionId not in self.__unions:
logging.error('Manager union [%s] not found' % unionId)
raise managerException('Union id not found')
if not self.__unions[unionId]['finish']: # some tasks are still running
raise managerException('Couldn\'t remove working union')
self.__unions.pop(unionId)
def getUnion(self, unionId: str) -> dict: # get union status (remove tasks when all completed) def getUnion(self, unionId: str) -> dict: # get union status (remove tasks when all completed)
if unionId not in self.__unions: if unionId not in self.__unions:
logging.error('Manager union [%s] not found' % unionId) logging.error('Manager union [%s] not found' % unionId)
raise RuntimeError('Union id not found') raise managerException('Union id not found')
if self.__unions[unionId]['finish']: # all tasks are finished
return self.__unions[unionId]
tasks = self.__unions[unionId]['items'] tasks = self.__unions[unionId]['items']
finishNum = 0 finishNum = 0
for taskId in tasks: for taskId in tasks:
@ -60,17 +71,15 @@ class Task(object):
'finish': False, 'finish': False,
'percent': round(finishNum / len(tasks), 2) 'percent': round(finishNum / len(tasks), 2)
} }
self.__unions.pop(unionId) # remove from union list self.__unions[unionId]['result'] = []
unionResult = [] # temporary storage
for taskId in tasks: for taskId in tasks:
task = self.__tasks[taskId] task = self.__tasks[taskId]
self.__tasks.pop(taskId) # remove from task list self.__tasks.pop(taskId) # remove from task list
unionResult.append(task['data']) self.__unions[unionId]['result'].append(task['data'])
logging.info('Manager release union [%s] -> %s' % (unionId, unionResult)) self.__unions[unionId]['finish'] = True
return { self.__unions[unionId].pop('items')
'finish': True, logging.info('Manager release union [%s] -> %s' % (unionId, self.__unions[unionId]['result']))
'result': unionResult return self.__unions[unionId]
}
def popTask(self) -> tuple[str or None, any]: # fetch a loaded task def popTask(self) -> tuple[str or None, any]: # fetch a loaded task
for taskId, task in self.__tasks.items(): for taskId, task in self.__tasks.items():
@ -78,13 +87,13 @@ class Task(object):
task['status'] = self.__TASK_RUNNING # set task status as running task['status'] = self.__TASK_RUNNING # set task status as running
logging.info('Manager pop task [%s] -> %s' % (taskId, task['data'])) logging.info('Manager pop task [%s] -> %s' % (taskId, task['data']))
return taskId, copy.deepcopy(task['data']) return taskId, copy.deepcopy(task['data'])
logging.debug('Manager has no more loaded tasks') logging.debug('Manager has no more task')
raise RuntimeError('No more tasks') raise managerException('No more tasks')
def finishTask(self, taskId: str, taskData: dict) -> None: # update task data when completed def finishTask(self, taskId: str, taskData: dict) -> None: # update task data when completed
if taskId not in self.__tasks: if taskId not in self.__tasks:
logging.error('Manager task [%s] not found' % taskId) logging.error('Manager task [%s] not found' % taskId)
raise RuntimeError('Task id not found') raise managerException('Task id not found')
self.__tasks[taskId]['data'] = copy.deepcopy(taskData) self.__tasks[taskId]['data'] = copy.deepcopy(taskData)
self.__tasks[taskId]['status'] = self.__TASK_FINISH # set task status as completed self.__tasks[taskId]['status'] = self.__TASK_FINISH # set task status as completed

23
Basis/Process.py

@ -8,6 +8,7 @@ import ctypes
import signal import signal
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Functions import genFlag from Basis.Functions import genFlag
from Basis.Exception import processException
from subprocess import Popen, STDOUT, DEVNULL from subprocess import Popen, STDOUT, DEVNULL
libcPaths = [ libcPaths = [
@ -73,7 +74,7 @@ class Process(object):
logging.error('[%s] %s already exist but not folder' % (self.id, self.workDir)) logging.error('[%s] %s already exist but not folder' % (self.id, self.workDir))
else: else:
logging.error('[%s] Unable to create new folder -> %s' % (self.id, self.workDir)) logging.error('[%s] Unable to create new folder -> %s' % (self.id, self.workDir))
raise RuntimeError('Working directory error') # fatal error raise processException('Working directory error') # fatal error
def __killProcess(self, killSignal: int) -> None: def __killProcess(self, killSignal: int) -> None:
try: try:
@ -134,10 +135,10 @@ class Process(object):
)) ))
if self.cmd is None: # ERROR CASE if self.cmd is None: # ERROR CASE
logging.error('[%s] Process miss start command' % self.id) logging.error('[%s] Process miss start command' % self.id)
raise RuntimeError('Miss start command') raise processException('Miss start command')
if self.__process is not None and self.__process.poll() is None: # ERROR CASE if self.__process is not None and self.__process.poll() is None: # ERROR CASE
logging.error('[%s] Process is still running' % self.id) logging.error('[%s] Process try to start but it is running' % self.id)
raise RuntimeError('Process is still running') raise processException('Process is still running')
if self.env is not None and 'PATH' not in self.env and '/' not in self.cmd[0]: # WARNING CASE if self.env is not None and 'PATH' not in self.env and '/' not in self.cmd[0]: # WARNING CASE
logging.warning('[%s] Executable file in relative path but miss PATH in environ' % self.id) logging.warning('[%s] Executable file in relative path but miss PATH in environ' % self.id)
if self.file is not None: # create and write file contents if self.file is not None: # create and write file contents
@ -153,11 +154,15 @@ class Process(object):
else: # discard all the output of sub process else: # discard all the output of sub process
stdout = DEVNULL stdout = DEVNULL
stderr = DEVNULL stderr = DEVNULL
self.__process = Popen( try:
self.cmd, env = self.env, self.__process = Popen(
stdout = stdout, stderr = stderr, self.cmd, env = self.env,
preexec_fn = None if libcPath is None else Process.__preExec stdout = stdout, stderr = stderr,
) preexec_fn = None if libcPath is None else Process.__preExec
)
except Exception as exp:
logging.error('[%s] Process unable to start -> %s' % (self.id, exp))
raise processException('Unable to start process')
logging.info('[%s] Process running -> PID = %i' % (self.id, self.__process.pid)) logging.info('[%s] Process running -> PID = %i' % (self.id, self.__process.pid))
def signal(self, signalNum: int) -> None: # send specified signal to sub process def signal(self, signalNum: int) -> None: # send specified signal to sub process

159
Basis/Test.py

@ -0,0 +1,159 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import requests
from threading import Thread
from Basis.Logger import logging
from Basis.Constant import WorkDir, TestHost, TestSite
from Basis.Functions import md5Sum, genFlag, hostFormat, checkPortStatus
Settings = {
'workDir': WorkDir,
'site': TestSite,
'serverBind': '',
'clientBind': '',
'host': '',
'cert': '',
'key': '',
}
def loadBind(serverV6: bool = False, clientV6: bool = False) -> None:
Settings['serverBind'] = '::1' if serverV6 else '127.0.0.1'
Settings['clientBind'] = '::1' if clientV6 else '127.0.0.1'
def waitPort(port: int, times: int = 50, delay: int = 100) -> bool: # wait until port occupied
for i in range(times):
if not checkPortStatus(port): # port occupied
return True
time.sleep(delay / 1000) # default wait 100ms * 50 => 5s
return False # timeout
def genCert(host: str, certInfo: dict, remark: str = 'ProxyC') -> None: # generate self-signed certificate
certOrgInfo = ['--organization', remark, '--organizationUnit', remark] # organization info
logging.critical('Load self-signed certificate')
os.system('mkdir -p %s' % Settings['workDir']) # make sure that work directory exist
# create CA data at first (by mad)
logging.critical('Creating CA certificate and key...')
os.system(' '.join( # generate CA certificate and privkey
['mad', 'ca', '--ca', certInfo['caCert'], '--key', certInfo['caKey'], '--commonName', remark] + certOrgInfo
))
# generate private key and sign certificate
logging.critical('Signing certificate...')
os.system(' '.join(['mad', 'cert', '--domain', host] + [ # generate certificate and privkey, then signed by CA
'--ca', certInfo['caCert'], '--ca_key', certInfo['caKey'],
'--cert', certInfo['cert'], '--key', certInfo['key'],
] + certOrgInfo))
# install CA certificate and record self-signed cert info
logging.critical('Installing CA certificate...')
os.system('cat %s >> /etc/ssl/certs/ca-certificates.crt' % certInfo['caCert']) # add into system's trust list
def loadCert(host: str = TestHost, certId: str = '') -> None: # load certificate
newCert = (certId == '')
certId = genFlag(length = 8) if certId == '' else certId
certInfo = {
'caCert': os.path.join(Settings['workDir'], 'proxyc_%s_ca.pem' % certId),
'caKey': os.path.join(Settings['workDir'], 'proxyc_%s_ca_key.pem' % certId),
'cert': os.path.join(Settings['workDir'], 'proxyc_%s_cert.pem' % certId),
'key': os.path.join(Settings['workDir'], 'proxyc_%s_cert_key.pem' % certId),
}
if newCert:
genCert(host, certInfo) # generate new certificate
Settings['host'] = host
Settings['cert'] = certInfo['cert']
Settings['key'] = certInfo['key']
logging.warning('Certificate load complete -> ID = %s' % certId)
def httpCheck(socksInfo: dict, url: str, testId: str, timeout: int = 10) -> None:
socksProxy = 'socks5://%s:%i' % (hostFormat(socksInfo['addr'], v6Bracket = True), socksInfo['port'])
try:
logging.debug('[%s] Http request via %s' % (testId, socksProxy))
request = requests.get(url, timeout = timeout, proxies = { # http request via socks5
'http': socksProxy,
'https': socksProxy,
})
request.raise_for_status() # throw error when server return 4xx or 5xx (don't actually need)
logging.info('[%s] %s -> ok' % (testId, socksProxy))
except Exception as exp:
logging.error('[%s] %s -> error\n%s' % (testId, socksProxy, exp)) # show detail of error reason
raise RuntimeError('Http request via socks5 failed')
def runTest(testInfo: dict, testUrl: str, testSelect: set or None, delay: int = 1) -> None:
testId = md5Sum(testInfo['caption'])[:12] # generate test ID
if testSelect is not None: # testSelect is None -> run all test
if testId not in testSelect: # skip unselected task
return
logging.warning('[%s] %s' % (testId, testInfo['caption'])) # show caption
logging.debug('[%s] Server ID -> %s | Client ID -> %s' % (
testId, testInfo['server'].id, testInfo['client'].id
))
testInfo['server'].id = testId + '-server'
testInfo['client'].id = testId + '-client'
# build server and client and wait them start
testInfo['server'].start() # start test server
if waitPort(testInfo['interface']['port']): # wait for server
logging.debug('[%s] Test server start complete' % testId)
testInfo['client'].start() # start test client
if waitPort(testInfo['socks']['port']): # wait for client
logging.debug('[%s] Test client start complete' % testId)
# start test process
try:
logging.debug('[%s] Test process start' % testId)
time.sleep(delay) # delay a short time before check
httpCheck(testInfo['socks'], testUrl, testId) # run http request test
testInfo['client'].quit() # clean up client
testInfo['server'].quit() # clean up server
except:
testInfo['client'].quit()
testInfo['server'].quit()
logging.warning('[%s] Client info' % testId)
logging.error('[%(id)s-server]\n▲ CMD => %(cmd)s\n▲ ENV => %(env)s\n▲ FILE => %(file)s\n%(output)s' % {
'id': testId,
'cmd': testInfo['client'].cmd,
'env': testInfo['client'].env,
'file': testInfo['client'].file,
'output': '-' * 96 + '\n' + testInfo['client'].output + '-' * 96,
})
logging.warning('[%s] Server info' % testId)
logging.error('[%(id)s-client]\n▲ CMD => %(cmd)s\n▲ ENV => %(env)s\n▲ FILE => %(file)s\n%(output)s' % {
'id': testId,
'cmd': testInfo['server'].cmd,
'env': testInfo['server'].env,
'file': testInfo['server'].file,
'output': '-' * 96 + '\n' + testInfo['server'].output + '-' * 96,
})
def Test(testIter: iter, threadNum: int, testUrl: str, testFilter: set or None = None) -> None:
threads = []
while True: # infinite loop
try:
for thread in threads:
if thread.is_alive(): # skip running thread
continue
threads.remove(thread) # remove dead thread
if len(threads) < threadNum:
for i in range(threadNum - len(threads)): # start threads within limit
thread = Thread( # create new thread
target = runTest,
args = (next(testIter), testUrl, testFilter)
)
thread.start()
threads.append(thread) # record thread info
time.sleep(0.1)
except StopIteration: # traverse completed
break
for thread in threads: # wait until all threads exit
thread.join()

26
Builder/Brook.py

@ -1,30 +1,29 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from Basis.Functions import hostFormat from Basis.Functions import v6AddBracket
def loadOrigin(proxyInfo: dict) -> list: # origin stream def loadOrigin(proxyInfo: dict) -> list: # origin stream
return ['client'] + [ return ['client'] + [
'--server', '%s:%i' % (hostFormat(proxyInfo['server'], v6Bracket = True), proxyInfo['port']), '--server', '%s:%i' % (v6AddBracket(proxyInfo['server']), proxyInfo['port']),
'--password', proxyInfo['passwd'], '--password', proxyInfo['passwd'],
] + (['--udpovertcp'] if proxyInfo['stream']['uot'] else []) ] + (['--udpovertcp'] if proxyInfo['stream']['uot'] else []) # add uot option
def loadWebsocket(proxyInfo: dict) -> list: def loadWebsocket(proxyInfo: dict) -> list: # websocket stream
isTls = proxyInfo['stream']['secure'] is not None isTls = proxyInfo['stream']['secure'] is not None # ws or wss
wsAddress = (('wss' if isTls else 'ws') + '://%s:%i%s') % ( wsAddress = (('wss' if isTls else 'ws') + '://%s:%i%s') % ( # websocket address
hostFormat(proxyInfo['stream']['host'], v6Bracket = True), proxyInfo['port'], proxyInfo['stream']['path'] v6AddBracket(proxyInfo['stream']['host']), proxyInfo['port'], proxyInfo['stream']['path']
) )
brookCommand = [ brookCommand = [
'wssclient' if isTls else 'wsclient', 'wssclient' if isTls else 'wsclient',
'--address', '%s:%i' % (hostFormat(proxyInfo['server'], v6Bracket = True), proxyInfo['port']), '--address', '%s:%i' % (v6AddBracket(proxyInfo['server']), proxyInfo['port']), # real address
'--password', proxyInfo['passwd'], '--password', proxyInfo['passwd'],
] + (['--withoutBrookProtocol'] if proxyInfo['stream']['raw'] else []) ] + (['--withoutBrookProtocol'] if proxyInfo['stream']['raw'] else []) # raw transmission on ws or wss
if not isTls: if not isTls:
return brookCommand + ['--wsserver', wsAddress] return brookCommand + ['--wsserver', wsAddress]
return brookCommand + ['--wssserver', wsAddress] + ( return brookCommand + ['--wssserver', wsAddress] + (
[] if proxyInfo['stream']['secure']['verify'] else ['--insecure'] [] if proxyInfo['stream']['secure']['verify'] else ['--insecure'] # add tls options
) )
@ -32,7 +31,6 @@ def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str,
brookCommand = ['brook', '--debug', '--listen', ':'] + { # debug module listen on random port brookCommand = ['brook', '--debug', '--listen', ':'] + { # debug module listen on random port
'origin': loadOrigin, 'origin': loadOrigin,
'ws': loadWebsocket, 'ws': loadWebsocket,
}[proxyInfo['stream']['type']](proxyInfo) + [ }[proxyInfo['stream']['type']](proxyInfo) # choose origin or websocket stream
'--socks5', '%s:%i' % (hostFormat(socksInfo['addr'], v6Bracket = True), socksInfo['port']) brookCommand += ['--socks5', '%s:%i' % (v6AddBracket(socksInfo['addr']), socksInfo['port'])]
]
return brookCommand, 'Config file %s no need' % configFile, {} # command, fileContent, envVar return brookCommand, 'Config file %s no need' % configFile, {} # command, fileContent, envVar

6
Builder/Hysteria.py

@ -2,19 +2,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
from Basis.Functions import hostFormat from Basis.Functions import v6AddBracket
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]: def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
hysteriaConfig = { hysteriaConfig = {
'server': '%s:%i' % (hostFormat(proxyInfo['server'], v6Bracket = True), proxyInfo['port']), 'server': '%s:%i' % (v6AddBracket(proxyInfo['server']), proxyInfo['port']),
'protocol': proxyInfo['protocol'], 'protocol': proxyInfo['protocol'],
'up_mbps': proxyInfo['up'], 'up_mbps': proxyInfo['up'],
'down_mbps': proxyInfo['down'], 'down_mbps': proxyInfo['down'],
'retry_interval': 2, 'retry_interval': 2,
'retry': 3, 'retry': 3,
'socks5': { 'socks5': {
'listen': '%s:%i' % (hostFormat(socksInfo['addr'], v6Bracket = True), socksInfo['port']) 'listen': '%s:%i' % (v6AddBracket(socksInfo['addr']), socksInfo['port'])
}, },
**({} if proxyInfo['obfs'] is None else { **({} if proxyInfo['obfs'] is None else {
'obfs': proxyInfo['obfs'] 'obfs': proxyInfo['obfs']

51
Builder/Shadowsocks.py

@ -2,7 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
from Basis.Constant import ssMethods, ssAllMethods from Basis.Exception import buildException
from Basis.Constant import ssMethods, ssAllMethods, mbedtlsMethods
def loadConfig(proxyInfo: dict, socksInfo: dict) -> dict: # load basic config option def loadConfig(proxyInfo: dict, socksInfo: dict) -> dict: # load basic config option
@ -20,70 +21,62 @@ def loadConfig(proxyInfo: dict, socksInfo: dict) -> dict: # load basic config o
return config return config
def pluginUdp(plugin: str, pluginParam: str) -> bool: # whether the plugin uses UDP def pluginUdp(plugin: str, pluginParam: str) -> bool: # whether the plugin uses udp
if plugin in ['obfs-local', 'simple-tls', 'ck-client', 'gq-client', 'mtt-client', 'rabbit-plugin']: if plugin in ['obfs-local', 'simple-tls', 'ck-client', 'gq-client', 'mtt-client', 'rabbit-plugin']:
return False # UDP is not used return False # UDP is not used
if plugin in ['v2ray-plugin', 'xray-plugin', 'gost-plugin']: if plugin in ['v2ray-plugin', 'xray-plugin', 'gost-plugin']:
if 'mode=quic' not in pluginParam.split(';'): # non-quic mode does not use UDP if 'mode=quic' not in pluginParam.split(';'): # non-quic mode does not use udp
return False return False
return True # UDP is assumed by default return True # udp is assumed by default
def ssRust(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list]: def ssRust(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo, socksInfo) config = loadConfig(proxyInfo, socksInfo)
if isUdp: # proxy UDP traffic if isUdp: # proxy udp traffic
config['mode'] = 'tcp_and_udp' config['mode'] = 'tcp_and_udp'
return config, ['ss-rust-local', '-v'] return config, ['ss-rust-local', '-v'], {'RUST_BACKTRACE': 'full'} # enable rust trace
def ssLibev(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list]: def ssLibev(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo, socksInfo) config = loadConfig(proxyInfo, socksInfo)
if isUdp: # proxy UDP traffic if isUdp: # proxy udp traffic
config['mode'] = 'tcp_and_udp' config['mode'] = 'tcp_and_udp'
return config, ['ss-libev-local', '-v'] return config, ['ss-libev-local', '-v'], {}
def ssPython(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list]: def ssPython(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo, socksInfo) config = loadConfig(proxyInfo, socksInfo)
mbedtlsMethods = [
'aes-128-cfb128',
'aes-192-cfb128',
'aes-256-cfb128',
'camellia-128-cfb128',
'camellia-192-cfb128',
'camellia-256-cfb128',
]
if config['method'] in mbedtlsMethods: # mbedtls methods should use prefix `mbedtls:` if config['method'] in mbedtlsMethods: # mbedtls methods should use prefix `mbedtls:`
config['method'] = 'mbedtls:' + config['method'] config['method'] = 'mbedtls:' + config['method']
if config['method'] in ['idea-cfb', 'seed-cfb']: # only older versions of openssl are supported if config['method'] in ['idea-cfb', 'seed-cfb']: # only older versions of openssl are supported
config['extra_opts'] = '--libopenssl=libcrypto.so.1.0.0' config['extra_opts'] = '--libopenssl=libcrypto.so.1.0.0'
if not isUdp: if not isUdp:
config['no_udp'] = True # UDP traffic is not proxied config['no_udp'] = True # udp traffic is not proxied
config['shadowsocks'] = 'ss-python-local' config['shadowsocks'] = 'ss-python-local'
return config, ['ss-bootstrap-local', '--debug', '-vv'] return config, ['ss-bootstrap-local', '--debug', '-vv'], {}
def ssPythonLegacy(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list]: def ssPythonLegacy(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo, socksInfo) config = loadConfig(proxyInfo, socksInfo)
if not isUdp: if not isUdp:
config['no_udp'] = True # UDP traffic is not proxied config['no_udp'] = True # udp traffic is not proxied
config['shadowsocks'] = 'ss-python-legacy-local' config['shadowsocks'] = 'ss-python-legacy-local'
return config, ['ss-bootstrap-local', '--debug', '-vv'] return config, ['ss-bootstrap-local', '--debug', '-vv'], {}
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]: def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
isUdp = True if proxyInfo['plugin'] is None else ( # UDP enabled when server without plugin isUdp = True if proxyInfo['plugin'] is None else ( # udp enabled when server without plugin
not pluginUdp(proxyInfo['plugin']['type'], proxyInfo['plugin']['param']) # UDP conflict status of plugins not pluginUdp(proxyInfo['plugin']['type'], proxyInfo['plugin']['param']) # udp conflict status of plugins
) )
if proxyInfo['method'] not in ssAllMethods: # unknown shadowsocks method if proxyInfo['method'] not in ssAllMethods: # unknown shadowsocks method
raise RuntimeError('Unknown shadowsocks method') raise buildException('Unknown shadowsocks method')
for client in ssMethods: # traverse all shadowsocks client for client in ssMethods: # traverse all shadowsocks client
if proxyInfo['method'] not in ssMethods[client]: if proxyInfo['method'] not in ssMethods[client]:
continue continue
ssConfig, ssClient = { ssConfig, ssClient, ssEnv = { # found appropriate client
'ss-rust': ssRust, 'ss-rust': ssRust,
'ss-libev': ssLibev, 'ss-libev': ssLibev,
'ss-python': ssPython, 'ss-python': ssPython,
'ss-python-legacy': ssPythonLegacy 'ss-python-legacy': ssPythonLegacy
}[client](proxyInfo, socksInfo, isUdp) # generate config file }[client](proxyInfo, socksInfo, isUdp) # generate config file
return ssClient + ['-c', configFile], json.dumps(ssConfig), {} # command, fileContent, envVar return ssClient + ['-c', configFile], json.dumps(ssConfig), ssEnv # command, fileContent, envVar

7
Builder/ShadowsocksR.py

@ -2,16 +2,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
from Basis.Exception import buildException
from Basis.Constant import ssrMethods, ssrProtocols, ssrObfuscations from Basis.Constant import ssrMethods, ssrProtocols, ssrObfuscations
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]: def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
if proxyInfo['method'] not in ssrMethods: if proxyInfo['method'] not in ssrMethods:
raise RuntimeError('Unknown shadowsocksr method') raise buildException('Unknown shadowsocksr method')
if proxyInfo['protocol'] not in ssrProtocols: if proxyInfo['protocol'] not in ssrProtocols:
raise RuntimeError('Unknown shadowsocksr protocol') raise buildException('Unknown shadowsocksr protocol')
if proxyInfo['obfs'] not in ssrObfuscations: if proxyInfo['obfs'] not in ssrObfuscations:
raise RuntimeError('Unknown shadowsocksr obfuscation') raise buildException('Unknown shadowsocksr obfuscation')
ssrConfig = { ssrConfig = {
'server': proxyInfo['server'], 'server': proxyInfo['server'],
'server_port': proxyInfo['port'], # type -> int 'server_port': proxyInfo['port'], # type -> int

2
Builder/Trojan.py

@ -13,7 +13,7 @@ def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str,
'address': proxyInfo['server'], 'address': proxyInfo['server'],
'port': proxyInfo['port'], 'port': proxyInfo['port'],
'password': proxyInfo['passwd'], 'password': proxyInfo['passwd'],
**Xray.xtlsFlow(proxyInfo['stream']) **Xray.xtlsFlow(proxyInfo['stream']) # add xtls flow option
}] }]
}, },
'streamSettings': Xray.loadStream(proxyInfo['stream']) 'streamSettings': Xray.loadStream(proxyInfo['stream'])

7
Builder/V2ray.py

@ -2,8 +2,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import copy import copy
from Basis.Exception import buildException
httpConfig = { httpConfig = { # http obfs configure in default
'type': 'http', 'type': 'http',
'request': { 'request': {
'version': '1.1', 'version': '1.1',
@ -24,7 +25,7 @@ httpConfig = {
} }
} }
kcpConfig = { kcpConfig = { # kcp options in default
'mtu': 1350, 'mtu': 1350,
'tti': 50, 'tti': 50,
'uplinkCapacity': 12, 'uplinkCapacity': 12,
@ -139,7 +140,7 @@ def loadStream(streamInfo: dict) -> dict:
'grpc': grpcStream, 'grpc': grpcStream,
} }
if streamInfo['type'] not in streamEntry: if streamInfo['type'] not in streamEntry:
raise RuntimeError('Unknown stream type') raise buildException('Unknown v2ray stream type')
streamObject = streamEntry[streamInfo['type']](streamInfo) streamObject = streamEntry[streamInfo['type']](streamInfo)
return { return {
**streamObject, **streamObject,

5
Builder/VLESS.py

@ -3,9 +3,12 @@
import json import json
from Builder import Xray from Builder import Xray
from Basis.Exception import buildException
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]: def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
if proxyInfo['method'] != 'none':
raise buildException('Unknown VLESS method')
outboundConfig = { outboundConfig = {
'protocol': 'vless', 'protocol': 'vless',
'settings': { 'settings': {
@ -15,7 +18,7 @@ def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str,
'users': [{ 'users': [{
'id': proxyInfo['id'], 'id': proxyInfo['id'],
'encryption': proxyInfo['method'], 'encryption': proxyInfo['method'],
**Xray.xtlsFlow(proxyInfo['stream']) **Xray.xtlsFlow(proxyInfo['stream']) # add xtls flow option
}] }]
}] }]
}, },

3
Builder/VMess.py

@ -4,11 +4,12 @@
import json import json
from Builder import V2ray from Builder import V2ray
from Basis.Constant import vmessMethods from Basis.Constant import vmessMethods
from Basis.Exception import buildException
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]: def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
if proxyInfo['method'] not in vmessMethods: if proxyInfo['method'] not in vmessMethods:
raise RuntimeError('Unknown vmess method') raise buildException('Unknown VMess method')
outboundConfig = { outboundConfig = {
'protocol': 'vmess', 'protocol': 'vmess',
'settings': { 'settings': {

7
Builder/Xray.py

@ -3,6 +3,7 @@
from Builder import V2ray from Builder import V2ray
from Basis.Constant import xtlsFlows from Basis.Constant import xtlsFlows
from Basis.Exception import buildException
loadConfig = V2ray.loadConfig loadConfig = V2ray.loadConfig
@ -11,7 +12,7 @@ def loadSecure(secureInfo: dict or None) -> dict: # TLS / XTLS encrypt config
if secureInfo is None: if secureInfo is None:
return {'security': 'none'} # without TLS / XTLS options return {'security': 'none'} # without TLS / XTLS options
if secureInfo['type'] not in ['tls', 'xtls']: if secureInfo['type'] not in ['tls', 'xtls']:
raise RuntimeError('Unknown secure type') raise buildException('Unknown xray secure type')
secureObject = { secureObject = {
'allowInsecure': not secureInfo['verify'] # whether verify server's certificate 'allowInsecure': not secureInfo['verify'] # whether verify server's certificate
} }
@ -53,7 +54,7 @@ def loadStream(streamInfo: dict) -> dict:
'grpc': V2ray.grpcStream, 'grpc': V2ray.grpcStream,
} }
if streamInfo['type'] not in streamEntry: if streamInfo['type'] not in streamEntry:
raise RuntimeError('Unknown stream type') raise buildException('Unknown xray stream type')
streamObject = streamEntry[streamInfo['type']](streamInfo) streamObject = streamEntry[streamInfo['type']](streamInfo)
return { return {
**streamObject, **streamObject,
@ -67,7 +68,7 @@ def xtlsFlow(streamInfo: dict or None) -> dict:
if streamInfo['secure']['type'] != 'xtls': # not XTLS secure type if streamInfo['secure']['type'] != 'xtls': # not XTLS secure type
return {} return {}
if streamInfo['secure']['flow'] not in xtlsFlows: if streamInfo['secure']['flow'] not in xtlsFlows:
raise RuntimeError('Unknown xtls flow') raise buildException('Unknown xtls flow')
return { return {
'flow': xtlsFlows[streamInfo['secure']['flow']] + ( # xtls-rprx-xxx 'flow': xtlsFlows[streamInfo['secure']['flow']] + ( # xtls-rprx-xxx
'-udp443' if streamInfo['secure']['udp443'] else '' # whether block udp/443 (disable http/3) '-udp443' if streamInfo['secure']['udp443'] else '' # whether block udp/443 (disable http/3)

14
Builder/__init__.py

@ -3,10 +3,13 @@
import os import os
import copy import copy
from Filter import Filter
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Basis.Functions import v6AddBracket
from Basis.Constant import WorkDir, PathEnv from Basis.Constant import WorkDir, PathEnv
from Basis.Functions import hostFormat, genFlag, getAvailablePort from Basis.Functions import genFlag, getAvailablePort
from Basis.Exception import buildException, filterException
from Builder import Brook from Builder import Brook
from Builder import VMess from Builder import VMess
@ -44,19 +47,19 @@ class Builder(object):
Attributes: Attributes:
id, proxyType, proxyInfo, socksAddr, socksPort, output id, proxyType, proxyInfo, socksAddr, socksPort, output
""" """
output = None output = None # output capture of proxy client (after process exit)
def __loadClient(self): def __loadClient(self):
logging.info('[%s] Builder load %s client at %s -> %s' % ( logging.info('[%s] Builder load %s client at %s -> %s' % (
self.id, self.proxyType, self.id, self.proxyType,
'socks5://%s:%i' % (hostFormat(self.socksAddr, v6Bracket = True), self.socksPort), self.proxyInfo 'socks5://%s:%i' % (v6AddBracket(self.socksAddr), self.socksPort), self.proxyInfo
)) ))
configFile = os.path.join( # config file path configFile = os.path.join( # config file path
WorkDir, self.id + clientEntry[self.proxyType][1] # workDir + taskId + file suffix WorkDir, self.id + clientEntry[self.proxyType][1] # workDir + taskId + file suffix
) )
logging.debug('[%s] Builder config file -> %s' % (self.id, configFile)) logging.debug('[%s] Builder config file -> %s' % (self.id, configFile))
command, fileContent, envVar = clientEntry[self.proxyType][0](self.proxyInfo, { # load client boot info command, fileContent, envVar = clientEntry[self.proxyType][0](self.proxyInfo, { # load client boot info
'addr': self.socksAddr, 'addr': self.socksAddr, # specify socks5 info
'port': self.socksPort, 'port': self.socksPort,
}, configFile) }, configFile)
envVar['PATH'] = PathEnv # add PATH env (some programs need it) envVar['PATH'] = PathEnv # add PATH env (some programs need it)
@ -69,8 +72,9 @@ class Builder(object):
self.id = genFlag(length = 12) if taskId == '' else taskId # load task ID self.id = genFlag(length = 12) if taskId == '' else taskId # load task ID
if proxyType not in clientEntry: if proxyType not in clientEntry:
logging.error('[%s] Builder receive unknown proxy type %s' % (self.id, proxyType)) logging.error('[%s] Builder receive unknown proxy type %s' % (self.id, proxyType))
raise RuntimeError('Unknown proxy type') raise buildException('Unknown proxy type')
self.proxyType = proxyType # proxy type -> ss / ssr / vmess ... self.proxyType = proxyType # proxy type -> ss / ssr / vmess ...
self.proxyInfo = Filter(proxyType, proxyInfo) # filter input proxy info
self.proxyInfo = copy.copy(proxyInfo) # connection info self.proxyInfo = copy.copy(proxyInfo) # connection info
self.socksAddr = bindAddr self.socksAddr = bindAddr
self.socksPort = getAvailablePort() # random port for socks5 exposed self.socksPort = getAvailablePort() # random port for socks5 exposed

23
Dockerfile

@ -31,7 +31,7 @@ FROM ${PYTHON_IMG} AS wheels
WORKDIR /wheels/ WORKDIR /wheels/
RUN apk add linux-headers RUN apk add linux-headers
COPY --from=build-base /apk/ /apk/ COPY --from=build-base /apk/ /apk/
RUN /apk/build-base && pip wheel colorlog flask IPy psutil pysocks requests salsa20 RUN /apk/build-base && pip wheel colorlog flask IPy psutil pysocks pyyaml requests salsa20
COPY --from=gevent /wheels/*.whl /wheels/ COPY --from=gevent /wheels/*.whl /wheels/
COPY --from=numpy /wheels/*.whl /wheels/ COPY --from=numpy /wheels/*.whl /wheels/
@ -53,7 +53,7 @@ COPY --from=build-base /apk/ /apk/
RUN wget https://github.com/shadowsocks/shadowsocks-rust/archive/refs/tags/v${SS_RUST}.tar.gz && \ RUN wget https://github.com/shadowsocks/shadowsocks-rust/archive/refs/tags/v${SS_RUST}.tar.gz && \
tar xf v${SS_RUST}.tar.gz && /apk/build-base tar xf v${SS_RUST}.tar.gz && /apk/build-base
WORKDIR ./shadowsocks-rust-${SS_RUST}/ WORKDIR ./shadowsocks-rust-${SS_RUST}/
RUN cargo update RUN cargo fetch
RUN cargo build --target-dir ./ --release --bin sslocal --bin ssserver \ RUN cargo build --target-dir ./ --release --bin sslocal --bin ssserver \
--features "stream-cipher aead-cipher-extra aead-cipher-2022 aead-cipher-2022-extra" && \ --features "stream-cipher aead-cipher-extra aead-cipher-2022 aead-cipher-2022-extra" && \
mv ./release/sslocal /tmp/ss-rust-local && mv ./release/ssserver /tmp/ss-rust-server && \ mv ./release/sslocal /tmp/ss-rust-local && mv ./release/ssserver /tmp/ss-rust-server && \
@ -169,7 +169,7 @@ RUN git submodule update --init --recursive && \
mv ./src/obfs-local ./src/obfs-server /plugins/ mv ./src/obfs-local ./src/obfs-server /plugins/
# Compile qtun # Compile qtun
WORKDIR ../qtun/ WORKDIR ../qtun/
RUN cargo update RUN cargo fetch
RUN cargo build --target-dir ./ --release && \ RUN cargo build --target-dir ./ --release && \
mv ./release/qtun-client ./release/qtun-server /plugins/ && \ mv ./release/qtun-client ./release/qtun-server /plugins/ && \
strip /plugins/* strip /plugins/*
@ -359,16 +359,17 @@ COPY --from=upx /upx/ /usr/
RUN upx -9 /tmp/clash RUN upx -9 /tmp/clash
# Download naiveproxy # Download naiveproxy
FROM ${ALPINE_IMG} AS naiveproxy FROM ${ALPINE_IMG} AS naive
ENV NAIVE_VERSION="v103.0.5060.53-3" ENV NAIVE_VERSION="v104.0.5112.79-2"
RUN apk add curl libgcc jq RUN apk add curl libgcc jq
RUN curl -sL https://api.github.com/repos/klzgrad/naiveproxy/releases/tags/${NAIVE_VERSION} \ RUN curl -sL https://api.github.com/repos/klzgrad/naiveproxy/releases/tags/${NAIVE_VERSION} \
| jq .assets | jq .[].name | grep naiveproxy-${NAIVE_VERSION}-openwrt-$(uname -m) \ | jq .assets | jq .[].name | grep naiveproxy-${NAIVE_VERSION}-openwrt-$(uname -m) \
| cut -b 2- | rev | cut -b 2- | rev | tac > list.dat | cut -b 2- | rev | cut -b 2- | rev | tac > list.dat
RUN echo -e "while read FILE_NAME;do\nwget https://github.com/klzgrad/naiveproxy/releases/download/\${NAIVE_VERSION}/\${FILE_NAME}\n \ RUN echo "while read FILE_NAME; do" >> naive.sh && \
tar xf \${FILE_NAME} && ldd ./\$(echo \$FILE_NAME | rev | cut -b 8- | rev)/naive\n \ echo "wget https://github.com/klzgrad/naiveproxy/releases/download/\${NAIVE_VERSION}/\${FILE_NAME}" >> naive.sh && \
[ \$? -eq 0 ] && cp ./\$(echo \$FILE_NAME | rev | cut -b 8- | rev)/naive /tmp/ && break\ndone < list.dat" > naiveproxy.sh && \ echo "tar xf \${FILE_NAME} && ldd ./\$(echo \$FILE_NAME | rev | cut -b 8- | rev)/naive" >> naive.sh && \
sh naiveproxy.sh echo "[ \$? -eq 0 ] && cp ./\$(echo \$FILE_NAME | rev | cut -b 8- | rev)/naive /tmp/ && break" >> naive.sh && \
echo "done < list.dat" >> naive.sh && sh naive.sh
COPY --from=build-base /apk/ /apk/ COPY --from=build-base /apk/ /apk/
RUN /apk/build-base && strip /tmp/naive RUN /apk/build-base && strip /tmp/naive
@ -475,9 +476,9 @@ COPY --from=trojan /tmp/trojan* /asset/usr/bin/
COPY --from=gost /tmp/gost* /asset/usr/bin/ COPY --from=gost /tmp/gost* /asset/usr/bin/
COPY --from=brook /tmp/brook /asset/usr/bin/ COPY --from=brook /tmp/brook /asset/usr/bin/
COPY --from=clash /tmp/clash /asset/usr/bin/ COPY --from=clash /tmp/clash /asset/usr/bin/
COPY --from=naive /tmp/naive /asset/usr/bin/
COPY --from=snell /tmp/snell-* /asset/usr/bin/ COPY --from=snell /tmp/snell-* /asset/usr/bin/
COPY --from=hysteria /tmp/hysteria /asset/usr/bin/ COPY --from=hysteria /tmp/hysteria /asset/usr/bin/
COPY --from=naiveproxy /tmp/naive /asset/usr/bin/
COPY --from=relaybaton /tmp/relaybaton /asset/usr/bin/ COPY --from=relaybaton /tmp/relaybaton /asset/usr/bin/
COPY --from=pingtunnel /tmp/pingtunnel /asset/usr/bin/ COPY --from=pingtunnel /tmp/pingtunnel /asset/usr/bin/
COPY --from=wireproxy /tmp/wireproxy /asset/usr/bin/ COPY --from=wireproxy /tmp/wireproxy /asset/usr/bin/
@ -492,4 +493,4 @@ RUN apk add --no-cache boost-program_options c-ares \
ln -s /usr/local/share/ProxyC/main.py /usr/bin/proxyc ln -s /usr/local/share/ProxyC/main.py /usr/bin/proxyc
COPY --from=asset /asset / COPY --from=asset /asset /
EXPOSE 7839 EXPOSE 7839
CMD ["proxyc"] ENTRYPOINT ["proxyc"]

10
Filter/__init__.py

@ -9,6 +9,7 @@ from Filter.Shadowsocks import ssFilter
from Filter.ShadowsocksR import ssrFilter from Filter.ShadowsocksR import ssrFilter
from Filter.TrojanGo import trojanGoFilter from Filter.TrojanGo import trojanGoFilter
from Filter.Hysteria import hysteriaFilter from Filter.Hysteria import hysteriaFilter
from Basis.Exception import filterException
filterEntry = { filterEntry = {
'ss': ssFilter, 'ss': ssFilter,
@ -23,5 +24,10 @@ filterEntry = {
def Filter(proxyType: str, proxyInfo: dict) -> dict: def Filter(proxyType: str, proxyInfo: dict) -> dict:
if proxyType not in filterEntry: if proxyType not in filterEntry:
raise RuntimeError('Unknown proxy type') raise filterException('Unknown proxy type')
return filterEntry[proxyType](proxyInfo) try:
return filterEntry[proxyType](proxyInfo)
except filterException as exp:
raise filterException(exp)
except:
raise filterException('Unknown filter error')

5
Tester/Brook.py

@ -4,9 +4,9 @@
import copy import copy
import itertools import itertools
from Builder import Brook from Builder import Brook
from Basis.Test import Settings
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Functions import hostFormat, genFlag, getAvailablePort from Basis.Functions import hostFormat, genFlag, getAvailablePort
@ -78,7 +78,7 @@ def loadTest(stream: dict) -> dict:
'port': proxyInfo['port'], 'port': proxyInfo['port'],
} }
} }
logging.debug('New brook test -> %s' % testInfo) logging.debug('New Brook test -> %s' % testInfo)
return testInfo return testInfo
@ -91,3 +91,4 @@ def load():
addStream(wsStream(isRaw, isSecure)) # websocket stream test addStream(wsStream(isRaw, isSecure)) # websocket stream test
for stream in streams: for stream in streams:
yield loadTest(stream) yield loadTest(stream)
logging.info('Brook test yield complete')

5
Tester/Hysteria.py

@ -5,9 +5,9 @@ import os
import json import json
import itertools import itertools
from Builder import Hysteria from Builder import Hysteria
from Basis.Test import Settings
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Constant import hysteriaProtocols from Basis.Constant import hysteriaProtocols
from Basis.Functions import hostFormat, genFlag, getAvailablePort from Basis.Functions import hostFormat, genFlag, getAvailablePort
@ -78,10 +78,11 @@ def loadTest(protocol: str, isObfs: bool, isAuth: bool) -> dict:
'port': proxyInfo['port'], 'port': proxyInfo['port'],
} }
} }
logging.debug('New hysteria test -> %s' % testInfo) logging.debug('New Hysteria test -> %s' % testInfo)
return testInfo return testInfo
def load(): def load():
for protocol, isObfs, isAuth in itertools.product(hysteriaProtocols, [False, True], [False, True]): for protocol, isObfs, isAuth in itertools.product(hysteriaProtocols, [False, True], [False, True]):
yield loadTest(protocol, isObfs, isAuth) yield loadTest(protocol, isObfs, isAuth)
logging.info('Hysteria test yield complete')

10
Tester/Plugin.py

@ -4,10 +4,10 @@
import os import os
import re import re
import json import json
from Basis.Test import Settings
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Basis.Constant import Plugins from Basis.Constant import Plugins
from Tester.Settings import Settings
from Basis.Functions import genFlag, hostFormat, getAvailablePort from Basis.Functions import genFlag, hostFormat, getAvailablePort
pluginParams = {} pluginParams = {}
@ -310,7 +310,7 @@ def paramFill(param: str) -> str:
return param return param
def load(proxyType: str): def load(proxyType: str) -> list:
if proxyType not in ['ss', 'trojan-go']: if proxyType not in ['ss', 'trojan-go']:
raise RuntimeError('Unknown proxy type for sip003 plugin') raise RuntimeError('Unknown proxy type for sip003 plugin')
pluginParams.update({ pluginParams.update({
@ -321,13 +321,14 @@ def load(proxyType: str):
'PASSWD': genFlag(length = 8), # random password for test 'PASSWD': genFlag(length = 8), # random password for test
'PATH': '/' + genFlag(length = 6), # random uri path for test 'PATH': '/' + genFlag(length = 6), # random uri path for test
}) })
result = []
cloakLoad() # init cloak config cloakLoad() # init cloak config
kcptunLoad() # init kcptun config kcptunLoad() # init kcptun config
for pluginType in pluginConfig: for pluginType in pluginConfig:
for pluginTest, pluginTestInfo in pluginConfig[pluginType].items(): # traverse all plugin test item for pluginTest, pluginTestInfo in pluginConfig[pluginType].items(): # traverse all plugin test item
pluginParams['RANDOM'] = genFlag(length = 8) # refresh RANDOM field pluginParams['RANDOM'] = genFlag(length = 8) # refresh RANDOM field
pluginParams['RABBIT_PORT'] = str(getAvailablePort()) # allocate port before rabbit plugin start pluginParams['RABBIT_PORT'] = str(getAvailablePort()) # allocate port before rabbit plugin start
yield { result.append({
'type': pluginType, 'type': pluginType,
'caption': pluginTest, 'caption': pluginTest,
'server': { # plugin info for server 'server': { # plugin info for server
@ -339,4 +340,5 @@ def load(proxyType: str):
'param': paramFill(pluginTestInfo[1]), 'param': paramFill(pluginTestInfo[1]),
}, },
'inject': ssInject if proxyType == 'ss' else trojanInject # for some special plugins 'inject': ssInject if proxyType == 'ss' else trojanInject # for some special plugins
} })
return result

14
Tester/Settings.py

@ -1,14 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from Basis.Constant import WorkDir
Settings = {
'workDir': WorkDir,
'site': 'www.bing.com',
'serverBind': '',
'clientBind': '',
'host': '',
'cert': '',
'key': '',
}

89
Tester/Shadowsocks.py

@ -7,11 +7,11 @@ import base64
import itertools import itertools
from Tester import Plugin from Tester import Plugin
from Builder import Shadowsocks from Builder import Shadowsocks
from Basis.Test import Settings
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Constant import ssMethods, ssAllMethods
from Basis.Functions import md5Sum, genFlag, getAvailablePort from Basis.Functions import md5Sum, genFlag, getAvailablePort
from Basis.Constant import PathEnv, ssMethods, ssAllMethods, mbedtlsMethods
def loadConfig(proxyInfo: dict) -> dict: # load basic config option def loadConfig(proxyInfo: dict) -> dict: # load basic config option
@ -27,30 +27,29 @@ def loadConfig(proxyInfo: dict) -> dict: # load basic config option
return config return config
def ssRust(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]: def addPathEnv(env: dict) -> dict:
return {
**env,
'PATH': PathEnv # add PATH env
}
def ssRust(proxyInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo) config = loadConfig(proxyInfo)
if isUdp: # proxy UDP traffic if isUdp: # proxy UDP traffic
config['mode'] = 'tcp_and_udp' config['mode'] = 'tcp_and_udp'
return config, ['ss-rust-server', '-v'] return config, ['ss-rust-server', '-v'], {'RUST_BACKTRACE': 'full'}
def ssLibev(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]: def ssLibev(proxyInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo) config = loadConfig(proxyInfo)
if isUdp: # proxy UDP traffic if isUdp: # proxy UDP traffic
config['mode'] = 'tcp_and_udp' config['mode'] = 'tcp_and_udp'
return config, ['ss-libev-server', '-v'] return config, ['ss-libev-server', '-v'], {}
def ssPython(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]: def ssPython(proxyInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo) config = loadConfig(proxyInfo)
mbedtlsMethods = [
'aes-128-cfb128',
'aes-192-cfb128',
'aes-256-cfb128',
'camellia-128-cfb128',
'camellia-192-cfb128',
'camellia-256-cfb128',
]
if config['method'] in mbedtlsMethods: # mbedtls methods should use prefix `mbedtls:` if config['method'] in mbedtlsMethods: # mbedtls methods should use prefix `mbedtls:`
config['method'] = 'mbedtls:' + config['method'] config['method'] = 'mbedtls:' + config['method']
if config['method'] in ['idea-cfb', 'seed-cfb']: # only older versions of openssl are supported if config['method'] in ['idea-cfb', 'seed-cfb']: # only older versions of openssl are supported
@ -58,15 +57,15 @@ def ssPython(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
if not isUdp: if not isUdp:
config['no_udp'] = True # UDP traffic is not proxied config['no_udp'] = True # UDP traffic is not proxied
config['shadowsocks'] = 'ss-python-server' config['shadowsocks'] = 'ss-python-server'
return config, ['ss-bootstrap-server', '--debug', '-vv'] return config, ['ss-bootstrap-server', '--debug', '-vv'], {}
def ssPythonLegacy(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]: def ssPythonLegacy(proxyInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo) config = loadConfig(proxyInfo)
if not isUdp: if not isUdp:
config['no_udp'] = True # UDP traffic is not proxied config['no_udp'] = True # UDP traffic is not proxied
config['shadowsocks'] = 'ss-python-legacy-server' config['shadowsocks'] = 'ss-python-legacy-server'
return config, ['ss-bootstrap-server', '--debug', '-vv'] return config, ['ss-bootstrap-server', '--debug', '-vv'], {}
def loadPassword(method: str) -> str: def loadPassword(method: str) -> str:
@ -79,7 +78,7 @@ def loadPassword(method: str) -> str:
def loadClient(ssType: str, configFile: str, proxyInfo: dict, socksInfo: dict) -> Process: def loadClient(ssType: str, configFile: str, proxyInfo: dict, socksInfo: dict) -> Process:
ssConfig, ssClient = { # generate client start command and its config file ssConfig, ssClient, ssEnv = { # generate client start command and its config file
'ss-rust': Shadowsocks.ssRust, 'ss-rust': Shadowsocks.ssRust,
'ss-libev': Shadowsocks.ssLibev, 'ss-libev': Shadowsocks.ssLibev,
'ss-python': Shadowsocks.ssPython, 'ss-python': Shadowsocks.ssPython,
@ -89,11 +88,11 @@ def loadClient(ssType: str, configFile: str, proxyInfo: dict, socksInfo: dict) -
return Process(Settings['workDir'], cmd = ssClient + ['-c', clientFile], file = { # load client process return Process(Settings['workDir'], cmd = ssClient + ['-c', clientFile], file = { # load client process
'path': clientFile, 'path': clientFile,
'content': json.dumps(ssConfig) 'content': json.dumps(ssConfig)
}, isStart = False) }, env = addPathEnv(ssEnv), isStart = False)
def loadServer(ssType: str, configFile: str, proxyInfo: dict) -> Process: def loadServer(ssType: str, configFile: str, proxyInfo: dict) -> Process:
ssConfig, ssServer = { # generate server start command and its config file ssConfig, ssServer, ssEnv = { # generate server start command and its config file
'ss-rust': ssRust, 'ss-rust': ssRust,
'ss-libev': ssLibev, 'ss-libev': ssLibev,
'ss-python': ssPython, 'ss-python': ssPython,
@ -103,7 +102,7 @@ def loadServer(ssType: str, configFile: str, proxyInfo: dict) -> Process:
return Process(Settings['workDir'], cmd = ssServer + ['-c', serverFile], file = { # load server process return Process(Settings['workDir'], cmd = ssServer + ['-c', serverFile], file = { # load server process
'path': serverFile, 'path': serverFile,
'content': json.dumps(ssConfig) 'content': json.dumps(ssConfig)
}, isStart = False) }, env = addPathEnv(ssEnv), isStart = False)
def loadTest(serverType: str, clientType: str, method: str, plugin: dict or None = None) -> dict: def loadTest(serverType: str, clientType: str, method: str, plugin: dict or None = None) -> dict:
@ -135,33 +134,37 @@ def loadTest(serverType: str, clientType: str, method: str, plugin: dict or None
} }
if plugin is not None: if plugin is not None:
testInfo['server'] = plugin['inject'](testInfo['server'], plugin) testInfo['server'] = plugin['inject'](testInfo['server'], plugin)
logging.debug('New shadowsocks test -> %s' % testInfo) logging.debug('New Shadowsocks test -> %s' % testInfo)
return testInfo return testInfo
def load(isExtra: bool = False): def loadCommon(pluginTest: list): # shadowsocks basic test
pluginTest = [] for method in ssAllMethods: # test every method for once
pluginIter = Plugin.load('ss') for ssType in ssMethods: # found the client which support this method
while True: if method not in ssMethods[ssType]: continue
try: yield loadTest(ssType, ssType, method) # ssType <-- method --> ssType
pluginTest.append(next(pluginIter)) # export data of plugin generator break # don't need other client
except StopIteration: for ssType in ssMethods: # test plugin for every shadowsocks project
break yield loadTest(ssType, ssType, ssMethods[ssType][0], pluginTest[0])
if not isExtra: # just test basic connection ssType = list(ssMethods.keys())[0] # choose the first one
for method in ssAllMethods: # test every method for once for plugin in pluginTest[1:]: # test every plugin (except the first one that has been checked)
for ssType in ssMethods: # found the client which support this method yield loadTest(ssType, ssType, ssMethods[ssType][0], plugin)
if method not in ssMethods[ssType]: continue
yield loadTest(ssType, ssType, method) # ssType <-- method --> ssType
break # don't need other client def loadExtra(pluginTest: list):
for ssType in ssMethods: # test plugin for every shadowsocks project
yield loadTest(ssType, ssType, ssMethods[ssType][0], pluginTest[0])
ssType = list(ssMethods.keys())[0] # choose the first one
for plugin in pluginTest[1:]: # test every plugin (except the first one that has been checked)
yield loadTest(ssType, ssType, ssMethods[ssType][0], plugin)
return
for ssServer in ssMethods: # traverse all shadowsocks type as server for ssServer in ssMethods: # traverse all shadowsocks type as server
for method, ssClient in itertools.product(ssMethods[ssServer], ssMethods): # supported methods and clients for method, ssClient in itertools.product(ssMethods[ssServer], ssMethods): # supported methods and clients
if method not in ssMethods[ssClient]: continue if method not in ssMethods[ssClient]: continue
yield loadTest(ssServer, ssClient, method) # ssServer <-- method --> ssClient yield loadTest(ssServer, ssClient, method) # ssServer <-- method --> ssClient
for ssType, plugin in itertools.product(ssMethods, pluginTest): # test every plugin with different ss project for ssType, plugin in itertools.product(ssMethods, pluginTest): # test every plugin with different ss project
yield loadTest(ssType, ssType, ssMethods[ssType][0], plugin) yield loadTest(ssType, ssType, ssMethods[ssType][0], plugin)
def load(isExtra: bool = False):
ssIter = (loadExtra if isExtra else loadCommon)(Plugin.load('ss'))
while True:
try:
yield next(ssIter)
except StopIteration:
break
logging.info('Shadowsocks test yield complete')

5
Tester/ShadowsocksR.py

@ -3,10 +3,10 @@
import os import os
import json import json
from Basis.Test import Settings
from Builder import ShadowsocksR from Builder import ShadowsocksR
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Functions import genFlag, getAvailablePort from Basis.Functions import genFlag, getAvailablePort
from Basis.Constant import ssrMethods, ssrProtocols, ssrObfuscations from Basis.Constant import ssrMethods, ssrProtocols, ssrObfuscations
@ -64,7 +64,7 @@ def loadTest(method: str, protocol: str, obfs: str) -> dict:
'port': proxyInfo['port'], 'port': proxyInfo['port'],
} }
} }
logging.debug('New shadowsocksr test -> %s' % testInfo) logging.debug('New ShadowsocksR test -> %s' % testInfo)
return testInfo return testInfo
@ -75,3 +75,4 @@ def load():
yield loadTest('aes-128-ctr', protocol, 'plain') yield loadTest('aes-128-ctr', protocol, 'plain')
for obfs in ssrObfuscations: for obfs in ssrObfuscations:
yield loadTest('aes-128-ctr', 'origin', obfs) yield loadTest('aes-128-ctr', 'origin', obfs)
logging.info('ShadowsocksR test yield complete')

5
Tester/Trojan.py

@ -5,10 +5,10 @@ import os
import json import json
from Tester import Xray from Tester import Xray
from Builder import Trojan from Builder import Trojan
from Basis.Test import Settings
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Basis.Constant import xtlsFlows from Basis.Constant import xtlsFlows
from Tester.Settings import Settings
from Basis.Functions import md5Sum, genFlag, getAvailablePort from Basis.Functions import md5Sum, genFlag, getAvailablePort
@ -110,7 +110,7 @@ def loadTest(stream: dict) -> dict:
'port': proxyInfo['port'], 'port': proxyInfo['port'],
} }
} }
logging.debug('New trojan test -> %s' % testInfo) logging.debug('New Trojan test -> %s' % testInfo)
return testInfo return testInfo
@ -119,3 +119,4 @@ def load():
yield loadBasicTest(streams[1]) # Trojan basic test -> TCP stream with TLS yield loadBasicTest(streams[1]) # Trojan basic test -> TCP stream with TLS
for stream in streams: # test all stream cases for stream in streams: # test all stream cases
yield loadTest(stream) yield loadTest(stream)
logging.info('Trojan test yield complete')

14
Tester/TrojanGo.py

@ -5,9 +5,9 @@ import os
import json import json
from Tester import Plugin from Tester import Plugin
from Builder import TrojanGo from Builder import TrojanGo
from Basis.Test import Settings
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Constant import trojanGoMethods from Basis.Constant import trojanGoMethods
from Basis.Functions import md5Sum, genFlag, getAvailablePort from Basis.Functions import md5Sum, genFlag, getAvailablePort
@ -85,18 +85,11 @@ def loadTest(wsObject: dict or None, ssObject: dict or None, plugin: dict or Non
} }
if plugin is not None: if plugin is not None:
testInfo['server'] = plugin['inject'](testInfo['server'], plugin) testInfo['server'] = plugin['inject'](testInfo['server'], plugin)
logging.debug('New trojan-go test -> %s' % testInfo) logging.debug('New Trojan-Go test -> %s' % testInfo)
return testInfo return testInfo
def load(): def load():
pluginTest = []
pluginIter = Plugin.load('trojan-go')
while True:
try:
pluginTest.append(next(pluginIter)) # export data of plugin generator
except StopIteration:
break
wsObject = { wsObject = {
'host': Settings['host'], 'host': Settings['host'],
'path': '/' + genFlag(length = 6), 'path': '/' + genFlag(length = 6),
@ -108,5 +101,6 @@ def load():
'passwd': genFlag(length = 8) 'passwd': genFlag(length = 8)
} }
yield loadTest(wsObject, None if ssObject['method'] == '' else ssObject, None) yield loadTest(wsObject, None if ssObject['method'] == '' else ssObject, None)
for plugin in pluginTest: # different plugin for trojan-go for plugin in Plugin.load('trojan-go'): # different plugin for trojan-go
yield loadTest(None, None, plugin) yield loadTest(None, None, plugin)
logging.info('Trojan-Go test yield complete')

2
Tester/V2ray.py

@ -3,8 +3,8 @@
import copy import copy
import itertools import itertools
from Basis.Test import Settings
from Basis.Functions import genFlag from Basis.Functions import genFlag
from Tester.Settings import Settings
from Basis.Constant import quicMethods, udpObfuscations from Basis.Constant import quicMethods, udpObfuscations
httpConfig = { httpConfig = {

5
Tester/VLESS.py

@ -5,10 +5,10 @@ import os
import json import json
from Tester import Xray from Tester import Xray
from Builder import VLESS from Builder import VLESS
from Basis.Test import Settings
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Basis.Constant import xtlsFlows from Basis.Constant import xtlsFlows
from Tester.Settings import Settings
from Basis.Functions import md5Sum, genUUID, getAvailablePort from Basis.Functions import md5Sum, genUUID, getAvailablePort
@ -71,7 +71,7 @@ def loadTest(stream: dict) -> dict:
'port': proxyInfo['port'], 'port': proxyInfo['port'],
} }
} }
logging.debug('New vless test -> %s' % testInfo) logging.debug('New VLESS test -> %s' % testInfo)
return testInfo return testInfo
@ -79,3 +79,4 @@ def load():
streams = Xray.loadStream() # load xray-core stream list streams = Xray.loadStream() # load xray-core stream list
for stream in streams: # test all stream cases for stream in streams: # test all stream cases
yield loadTest(stream) yield loadTest(stream)
logging.info('VLESS test yield complete')

5
Tester/VMess.py

@ -6,9 +6,9 @@ import json
import itertools import itertools
from Tester import V2ray from Tester import V2ray
from Builder import VMess from Builder import VMess
from Basis.Test import Settings
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Constant import PathEnv, vmessMethods from Basis.Constant import PathEnv, vmessMethods
from Basis.Functions import md5Sum, genUUID, getAvailablePort from Basis.Functions import md5Sum, genUUID, getAvailablePort
@ -69,7 +69,7 @@ def loadTest(method: str, aid: int, stream: dict) -> dict:
'port': proxyInfo['port'], 'port': proxyInfo['port'],
} }
} }
logging.debug('New vmess test -> %s' % testInfo) logging.debug('New VMess test -> %s' % testInfo)
return testInfo return testInfo
@ -79,3 +79,4 @@ def load():
yield loadTest(method, aid, streams[0]) yield loadTest(method, aid, streams[0])
for stream in streams[1:]: # skip first stream that has benn checked for stream in streams[1:]: # skip first stream that has benn checked
yield loadTest('auto', 0, stream) # aead with auto security yield loadTest('auto', 0, stream) # aead with auto security
logging.info('VMess test yield complete')

2
Tester/Xray.py

@ -4,8 +4,8 @@
import copy import copy
import itertools import itertools
from Tester import V2ray from Tester import V2ray
from Basis.Test import Settings
from Basis.Functions import genFlag from Basis.Functions import genFlag
from Tester.Settings import Settings
from Basis.Constant import xtlsFlows, quicMethods, udpObfuscations from Basis.Constant import xtlsFlows, quicMethods, udpObfuscations
loadConfig = V2ray.loadConfig loadConfig = V2ray.loadConfig

128
Tester/__init__.py

@ -1,14 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import time
import requests
from threading import Thread
from Basis.Logger import logging
from Tester.Settings import Settings
from Basis.Functions import md5Sum, genFlag, hostFormat, checkPortStatus
from Tester import Brook from Tester import Brook
from Tester import VMess from Tester import VMess
from Tester import VLESS from Tester import VLESS
@ -18,7 +10,7 @@ from Tester import Hysteria
from Tester import Shadowsocks from Tester import Shadowsocks
from Tester import ShadowsocksR from Tester import ShadowsocksR
entry = { testEntry = {
'ss': Shadowsocks.load(), 'ss': Shadowsocks.load(),
'ss-all': Shadowsocks.load(isExtra = True), 'ss-all': Shadowsocks.load(isExtra = True),
'ssr': ShadowsocksR.load(), 'ssr': ShadowsocksR.load(),
@ -29,121 +21,3 @@ entry = {
'brook': Brook.load(), 'brook': Brook.load(),
'hysteria': Hysteria.load(), 'hysteria': Hysteria.load(),
} }
def waitPort(port: int, times: int = 100, delay: int = 100) -> bool: # wait until port occupied
for i in range(times):
if not checkPortStatus(port): # port occupied
return True
time.sleep(delay / 1000) # default wait 100ms
return False # timeout
def httpCheck(socksInfo: dict, url: str, testId: str, timeout: int = 10) -> None:
socksProxy = 'socks5://%s:%i' % (hostFormat(socksInfo['addr'], v6Bracket = True), socksInfo['port'])
try:
proxy = {
'http': socksProxy,
'https': socksProxy,
}
request = requests.get(url, timeout = timeout, proxies = proxy)
request.raise_for_status()
logging.info('[%s] %s -> ok' % (testId, socksProxy))
except Exception as exp:
logging.error('[%s] %s -> error' % (testId, socksProxy))
logging.error('requests exception\n' + str(exp))
raise RuntimeError('socks5 test failed')
def runTest(testInfo: dict, testUrl: str, testFilter: set or None, delay: int = 1) -> None:
testInfo['hash'] = md5Sum(testInfo['caption'])[:12]
if testFilter is not None and testInfo['hash'] not in testFilter: return
logging.warning('[%s] %s' % (testInfo['hash'], testInfo['caption']))
testInfo['server'].start() # start test server
if waitPort(testInfo['interface']['port']): # wait for server
logging.debug('server start complete')
testInfo['client'].start() # start test client
if waitPort(testInfo['socks']['port']): # wait for client
logging.debug('client start complete')
try:
logging.debug('start test process')
time.sleep(delay)
httpCheck(testInfo['socks'], testUrl, testInfo['hash'])
testInfo['client'].quit()
testInfo['server'].quit()
except:
# client debug info
testInfo['client'].quit()
logging.warning('[%s] client info' % testInfo['hash'])
logging.error('command -> %s' % testInfo['client'].cmd)
logging.error('envVar -> %s' % testInfo['client'].env)
logging.error('file -> %s' % testInfo['client'].file)
logging.warning('[%s] client capture output' % testInfo['hash'])
logging.error('\n%s' % testInfo['client'].output)
# server debug info
testInfo['server'].quit()
logging.warning('[%s] server info' % testInfo['hash'])
logging.error('command -> %s' % testInfo['server'].cmd)
logging.error('envVar -> %s' % testInfo['server'].env)
logging.error('file -> %s' % testInfo['server'].file)
logging.warning('[%s] server capture output' % testInfo['hash'])
logging.error('\n%s' % testInfo['server'].output)
def test(testIter: iter, threadNum: int, testUrl: str, testFilter: set or None = None) -> None:
threads = []
while True: # infinite loop
try:
for thread in threads:
if thread.is_alive(): continue
threads.remove(thread) # remove dead thread
if len(threads) < threadNum:
for i in range(threadNum - len(threads)): # start threads within limit
thread = Thread( # create new thread
target = runTest,
args = (next(testIter), testUrl, testFilter)
)
thread.start()
threads.append(thread) # record thread info
time.sleep(0.1)
except StopIteration: # traverse completed
break
for thread in threads: # wait until all threads exit
thread.join()
def loadBind(serverV6: bool = False, clientV6: bool = False) -> None:
Settings['serverBind'] = '::1' if serverV6 else '127.0.0.1'
Settings['clientBind'] = '::1' if clientV6 else '127.0.0.1'
def loadCert(host: str = 'proxyc.net', remark: str = 'ProxyC') -> None:
loadPath = lambda x: os.path.join(Settings['workDir'], x)
certFlag = genFlag(length = 8)
caCert = loadPath('proxyc_%s_ca.pem' % certFlag)
caKey = loadPath('proxyc_%s_ca_key.pem' % certFlag)
cert = loadPath('proxyc_%s_cert.pem' % certFlag)
key = loadPath('proxyc_%s_cert_key.pem' % certFlag)
logging.critical('Load self-signed certificate')
os.system('mkdir -p %s' % Settings['workDir']) # create work directory
logging.critical('Creating CA certificate and key...')
os.system(' '.join(['mad', 'ca'] + [ # generate CA certificate and privkey
'--ca', caCert, '--key', caKey,
'--commonName', remark,
'--organization', remark,
'--organizationUnit', remark,
]))
logging.critical('Signing certificate...')
os.system(' '.join(['mad', 'cert'] + [ # generate certificate and privkey, then signed by CA
'--ca', caCert, '--ca_key', caKey,
'--cert', cert, '--key', key,
'--domain', host,
'--organization', remark,
'--organizationUnit', remark,
]))
logging.critical('Installing CA certificate...')
os.system('cat %s >> /etc/ssl/certs/ca-certificates.crt' % caCert) # add into system's trust list
Settings['host'] = host
Settings['cert'] = cert
Settings['key'] = key
logging.warning('Certificate loading complete')

8
env.yaml

@ -0,0 +1,8 @@
version: 'v0.1'
loglevel: 'INFO'
dir: '/tmp/ProxyC'
dns: null
api:
port: 7839
path: '/'
token: ''

135
main.py

@ -1,41 +1,140 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
import copy
import time import time
import _thread import _thread
from Basis import DnsProxy import argparse
import compileall
from Basis import Constant
from Basis.Exception import checkException
def mainArgParse(rawArgs: list) -> argparse.Namespace:
mainParser = argparse.ArgumentParser(description = 'Start running API server')
mainParser.add_argument('--log', type = str, default = Constant.LogLevel, help = 'output log level')
mainParser.add_argument('--dns', type = str, default = Constant.DnsServer, nargs = '+', help = 'specify dns server')
mainParser.add_argument('--port', type = int, default = Constant.ApiPort, help = 'port for running')
mainParser.add_argument('--path', type = str, default = Constant.ApiPath, help = 'root path for api server')
mainParser.add_argument('--token', type = str, default = Constant.ApiToken, help = 'token for api server')
mainParser.add_argument('--thread', type = int, default = Constant.CheckThread, help = 'number of check thread')
mainParser.add_argument('-v', '--version', help = 'show version', action = 'store_true')
return mainParser.parse_args(rawArgs)
def testArgParse(rawArgs: list) -> argparse.Namespace:
testParser = argparse.ArgumentParser(description = 'Test that each function is working properly')
testParser.add_argument('PROTOCOL', type = str, help = 'test protocol name')
testParser.add_argument('-a', '--all', help = 'test extra shadowsocks items', action = 'store_true')
testParser.add_argument('-6', '--ipv6', help = 'test on ipv6 network', action = 'store_true')
testParser.add_argument('--debug', help = 'enable debug log level', action = 'store_true')
testParser.add_argument('--url', type = str, default = 'http://baidu.com', help = 'http request url')
testParser.add_argument('--cert', type = str, default = '', help = 'specify the certificate id')
testParser.add_argument('--thread', type = int, default = 16, help = 'thread number in check process')
testParser.add_argument('--select', type = str, nargs = '+', help = 'select id list for test')
return testParser.parse_args(rawArgs)
testArgs = None
testMode = False
inputArgs = copy.copy(sys.argv)
if len(inputArgs) >= 0: # remove first arg (normally file name)
inputArgs.pop(0)
if len(inputArgs) != 0 and inputArgs[0].lower() == 'test': # test mode
inputArgs.pop(0) # remove `test`
if len(inputArgs) == 0 or inputArgs[0].startswith('-'): # no protocol is specified
inputArgs = ['all'] + inputArgs
testArgs = testArgParse(inputArgs)
Constant.LogLevel = 'debug' if testArgs.debug else 'warning'
testMode = True
else:
mainArgs = mainArgParse(inputArgs)
if mainArgs.version: # output version and exit
print('ProxyC version -> %s' % Constant.Version)
sys.exit(0)
Constant.LogLevel = mainArgs.log # overwrite global options
Constant.DnsServer = mainArgs.dns
Constant.ApiPort = mainArgs.port
Constant.ApiPath = mainArgs.path
Constant.ApiToken = mainArgs.token
Constant.CheckThread = mainArgs.thread
from Tester import testEntry
from Basis.Check import Check from Basis.Check import Check
from Basis import Api, DnsProxy
from Basis.Logger import logging from Basis.Logger import logging
from Basis.Manager import Manager from Basis.Manager import Manager
from Basis.Api import startServer from Basis.Test import Test, loadBind, loadCert
from Basis.Constant import Version
from Basis.Compile import startCompile
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
# dnsServers = None
dnsServers = ['223.5.5.5', '119.28.28.28'] def pythonCompile(dirRange: str = '/') -> None: # python optimize compile
for optimize in [-1, 1, 2]:
compileall.compile_dir(dirRange, quiet = 1, optimize = optimize)
logging.warning('Python optimize compile -> %s (level = %i)' % (dirRange, optimize))
def runCheck(taskId: str, taskInfo: dict) -> None: def runCheck(taskId: str, taskInfo: dict) -> None:
checkResult = Check(taskId, taskInfo) success = True
logging.warning('[%s] Task finish' % taskId) checkResult = {}
Manager.finishTask(taskId, checkResult) try:
checkResult = Check(taskId, taskInfo) # check by task info
logging.warning('[%s] Task finish' % taskId)
except checkException as exp:
success = False
logging.error('[%s] Task error -> %s' % (taskId, exp))
except:
success = False
logging.error('[%s] Task error -> Unknown error' % taskId)
finally:
if not success: # got some error in check process
taskInfo.pop('check')
checkResult = {
**taskInfo,
'success': False,
}
Manager.finishTask(taskId, checkResult) # commit check result
def loopCheck(threadNum: int = 16) -> None: def loop(threadNum: int) -> None:
threadPool = ThreadPoolExecutor(max_workers = threadNum) logging.warning('Loop check start -> %i threads' % threadNum)
threadPool = ThreadPoolExecutor(max_workers = threadNum) # init thread pool
while True: while True:
try: try:
taskId, taskInfo = Manager.popTask() taskId, taskInfo = Manager.popTask() # pop a task
logging.warning('[%s] Load new task' % taskId) logging.warning('[%s] Load new task' % taskId)
except: # no more task except: # no more task
time.sleep(2) time.sleep(2)
continue continue
threadPool.submit(runCheck, taskId, taskInfo) threadPool.submit(runCheck, taskId, taskInfo) # submit into thread pool
if testMode: # test mode
loadBind(serverV6 = testArgs.ipv6, clientV6 = testArgs.ipv6) # ipv4 / ipv6 (127.0.0.1 / ::1)
loadCert(certId = testArgs.cert) # cert config
logging.critical('TEST ITEM: %s' % testArgs.PROTOCOL)
logging.critical('SELECT: ' + str(testArgs.select))
logging.critical('URL: %s' % testArgs.url)
logging.critical('THREAD NUMBER: %i' % testArgs.thread)
logging.critical('-' * 32 + ' TEST START ' + '-' * 32)
if testArgs.PROTOCOL == 'all': # run all test items
for item in testEntry:
if item == ('ss' if testArgs.all else 'ss-all'): # skip ss / ss-all
continue
logging.critical('TEST ITEM -> ' + item)
Test(testEntry[item], testArgs.thread, testArgs.url, testArgs.select)
else: # run single item
if testArgs.PROTOCOL == 'ss' and testArgs.all: # test shadowsocks extra items
testItem = 'ss-all'
Test(testEntry[testArgs.PROTOCOL], testArgs.thread, testArgs.url, testArgs.select)
logging.critical('-' * 32 + ' TEST COMPLETE ' + '-' * 32)
sys.exit(0) # test complete
logging.warning('ProxyC starts running (%s)' % Version) logging.warning('ProxyC starts running (%s)' % Constant.Version)
_thread.start_new_thread(startCompile, ('/usr', )) # python compile (generate .pyc file) _thread.start_new_thread(pythonCompile, ('/usr',)) # python compile (generate .pyc file)
_thread.start_new_thread(DnsProxy.start, (dnsServers, 53)) # start dns server _thread.start_new_thread(DnsProxy.start, (Constant.DnsServer, 53)) # start dns server
_thread.start_new_thread(loopCheck, ()) # start loop check _thread.start_new_thread(loop, (Constant.CheckThread, )) # start check loop
startServer(apiToken = '') # start api server Api.startServer() # start api server

66
test.py

@ -1,66 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import Tester
from Basis.Logger import logging
threadNum = 16
testItem = None
testFilter = None
testUrl = 'http://baidu.com'
helpMsg = '''
./test.py [ITEM] [OPTIONS]
[ITEM]: ss / ssr / vmess / vless / trojan / trojan-go / brook / hysteria
[OPTIONS]:
--thread NUM thread number
--url URL http check url
--filter ID1[,ID2...] test the specified id
--all test extra shadowsocks items
--ipv6 test on ipv6 network
--help show this message
'''
def getArg(field: str) -> str or None:
try:
index = sys.argv.index(field)
return sys.argv[index + 1]
except:
return None
if '--help' in sys.argv:
print(helpMsg)
sys.exit(0)
if len(sys.argv) > 1 and not sys.argv[1].startswith('--'):
testItem = sys.argv[1]
if getArg('--url') is not None:
testUrl = getArg('--url')
if getArg('--thread') is not None:
threadNum = int(getArg('--thread'))
if getArg('--filter') is not None:
testFilter = set(getArg('--filter').split(','))
isV6 = '--ipv6' in sys.argv
Tester.loadBind(serverV6 = isV6, clientV6 = isV6) # ipv4 / ipv6 (127.0.0.1 / ::1)
Tester.loadCert('proxyc.net', 'ProxyC') # default cert config
logging.critical('TEST ITEM: ' + ('all' if testItem is None else testItem))
logging.critical('FILTER: %s' % testFilter)
logging.critical('URL: ' + testUrl)
logging.critical('THREAD NUMBER: %i' % threadNum)
logging.critical('-------------------------------- TEST START --------------------------------')
if testItem is not None:
if testItem == 'ss' and '--all' in sys.argv:
testItem = 'ss-all'
Tester.test(Tester.entry[testItem], threadNum, testUrl, testFilter)
else:
for item in Tester.entry:
if item == ('ss' if '--all' in sys.argv else 'ss-all'): # skip ss / ss-all
continue
logging.critical('TEST ITEM -> ' + item)
Tester.test(Tester.entry[item], threadNum, testUrl, testFilter)
logging.critical('-------------------------------- TEST COMPLETE --------------------------------')
Loading…
Cancel
Save