Browse Source

update: enhance Tester module

master
dnomd343 2 years ago
parent
commit
f30324c39e
  1. 2
      Basis/Methods.py
  2. 46
      Basis/Process.py
  3. 25
      Builder/__init__.py
  4. 156
      Tester/Shadowsocks.py
  5. 167
      test.py

2
Basis/Methods.py

@ -63,7 +63,7 @@ ssMethods = { # methods support of different Shadowsocks project
ssAllMethods = set()
[ssAllMethods.update(ssMethods[x]) for x in ssMethods]
ssAllMethods = list(ssAllMethods) # methods of Shadowsocks
ssAllMethods = sorted(list(ssAllMethods)) # methods of Shadowsocks
ssrMethods = [ # methods of ShadowsocksR
'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr',

46
Basis/Process.py

@ -49,7 +49,7 @@ class Process(object):
isStart: Start the process after class init complete.
Attributes:
id, workDir, output
id, cmd, env, file, workDir, output
"""
output = None # sub process output if capture is True
__capture = None # capture the sub process output or not
@ -97,34 +97,34 @@ class Process(object):
cmd: str or list or None = None, env: dict or None = None, file: dict or list or None = None) -> None:
self.id = genFlag(length = 12) if taskId == '' else taskId
self.workDir = workDir
self.__env = copy.copy(env) # depth = 1
self.__cmd = copy.copy([cmd] if type(cmd) == str else cmd) # depth = 1
self.__file = copy.deepcopy([file] if type(file) == dict else file) # depth = 2
self.env = copy.copy(env) # depth = 1
self.cmd = copy.copy([cmd] if type(cmd) == str else cmd) # depth = 1
self.file = copy.deepcopy([file] if type(file) == dict else file) # depth = 2
self.__checkWorkDir() # ensure the working direction is normal
logging.debug('[%s] Process command -> %s (%s)' % (self.id, self.__cmd, self.__env))
if self.__file is not None:
if len(self.__file) > 1:
logging.debug('[%s] Manage %i files' % (self.id, len(self.__file)))
for file in self.__file: # traverse all files
logging.debug('[%s] Process command -> %s (%s)' % (self.id, self.cmd, self.env))
if self.file is not None:
if len(self.file) > 1:
logging.debug('[%s] Manage %i files' % (self.id, len(self.file)))
for file in self.file: # traverse all files
if not isStart: # don't print log twice
logging.debug('[%s] File %s -> %s' % (self.id, file['path'], file['content']))
if isStart:
self.start()
def setCmd(self, cmd: str or list) -> None:
self.__cmd = copy.copy([cmd] if type(cmd) == str else cmd)
logging.info('[%s] Process setting command -> %s' % (self.id, self.__cmd))
self.cmd = copy.copy([cmd] if type(cmd) == str else cmd)
logging.info('[%s] Process setting command -> %s' % (self.id, self.cmd))
def setEnv(self, env: dict or None) -> None:
self.__env = copy.copy(env)
logging.info('[%s] Process setting environ -> %s' % (self.id, self.__env))
self.env = copy.copy(env)
logging.info('[%s] Process setting environ -> %s' % (self.id, self.env))
def setFile(self, file: dict or list or None) -> None:
self.__file = copy.deepcopy([file] if type(file) == dict else file)
if self.__file is None:
self.file = copy.deepcopy([file] if type(file) == dict else file)
if self.file is None:
logging.info('[%s] Process setting file -> None' % self.id)
return
for file in self.__file: # traverse all files
for file in self.file: # traverse all files
logging.info('[%s] Process setting file %s -> %s' % (self.id, file['path'], file['content']))
def start(self, isCapture: bool = True) -> None:
@ -132,16 +132,16 @@ class Process(object):
logging.debug('[%s] Process ready to start (%s)' % (self.id, (
'with output capture' if self.__capture else 'without output capture'
)))
if self.__cmd is None: # ERROR CASE
if self.cmd is None: # ERROR CASE
logging.error('[%s] Process miss start command' % self.id)
raise RuntimeError('miss start command')
if self.__process is not None and self.__process.poll() is None: # ERROR CASE
logging.error('[%s] Sub process is still running' % self.id)
raise RuntimeError('sub 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)
if self.__file is not None: # create and write file contents
for file in self.__file:
if self.file is not None: # create and write file contents
for file in self.file:
with open(file['path'], 'w', encoding = 'utf-8') as fileObject: # save file content
fileObject.write(file['content'])
logging.debug('[%s] File %s -> %s' % (self.id, file['path'], file['content']))
@ -153,7 +153,7 @@ class Process(object):
else: # discard all the output of sub process
stdout = DEVNULL
stderr = DEVNULL
self.__process = Popen(self.__cmd, env = self.__env, stdout = stdout,
self.__process = Popen(self.cmd, env = self.env, stdout = stdout,
stderr = stderr, preexec_fn = None if libcPath is None else Process.__preExec)
logging.info('[%s] Process running -> PID = %i' % (self.id, self.__process.pid))
@ -195,6 +195,6 @@ class Process(object):
self.__deleteFile(self.__logfile)
except:
logging.error('[%s] Failed to read capture log file -> %s' % (self.id, self.__logfile))
if self.__file is not None: # with config file
for file in self.__file:
if self.file is not None: # with config file
for file in self.file:
self.__deleteFile(file['path'])

25
Builder/__init__.py

@ -11,18 +11,14 @@ from Basis.Logger import logging
from Basis.Process import Process
from Basis.Functions import genFlag, getAvailablePort
default = {
'workDir': '/tmp/ProxyC',
'bindAddr': '127.0.0.1',
'binDir': '/bin:/usr/bin:/usr/local/bin',
}
class Builder(object):
""" Build the proxy client process and expose socks5 port.
Arguments:
proxy: Proxy node information.
proxyType: Proxy node type.
proxyInfo: Proxy node information.
bind: Socks5 proxy bind address.
@ -30,14 +26,12 @@ class Builder(object):
taskId: Task ID, defaults to 12 random characters length.
isStart: Start the process after class init complete.
Attributes:
id, proxyType, proxyInfo, socksAddr, socksPort, output
"""
output = None
def __loadClient(self, isStart: bool):
def __loadClient(self):
loadFunction = {
'ss': Shadowsocks.load,
'ssr': ShadowsocksR.load,
@ -52,23 +46,22 @@ class Builder(object):
'addr': self.socksAddr,
'port': self.socksPort,
}, configFile)
envVar['PATH'] = default['binDir']
envVar['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin'
fileObject = {
'path': configFile,
'content': fileContent
}
self.__process = Process(self.__workDir, taskId = self.id,
isStart = isStart, cmd = command, env = envVar, file = fileObject)
self.__process = Process(self.__workDir, taskId = self.id, cmd = command, env = envVar, file = fileObject)
def __init__(self, proxyType: str, proxyInfo: dict, taskId: str = '', isStart: bool = True,
bind: str = default['bindAddr'], workDir: str = default['workDir']) -> None:
def __init__(self, proxyType: str, proxyInfo: dict, taskId: str = '',
bind: str = '127.0.0.1', workDir: str = '/tmp/ProxyC') -> None:
self.id = genFlag(length = 12) if taskId == '' else taskId
self.__workDir = workDir
self.proxyType = proxyType
self.proxyInfo = copy.copy(proxyInfo)
self.socksAddr = bind
self.socksPort = getAvailablePort()
self.__loadClient(isStart)
self.__loadClient()
def status(self) -> bool:
return self.__process.status()

156
Tester/Shadowsocks.py

@ -0,0 +1,156 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import json
import base64
from Builder import Shadowsocks
from Basis.Logger import logging
from Basis.Process import Process
from Basis.Methods import ssMethods, ssAllMethods
from Basis.Functions import genFlag, getAvailablePort
settings = {
'serverBind': '127.0.0.1',
'clientBind': '127.0.0.1', # aka socks5 address
'workDir': '/tmp/ProxyC'
}
def loadConfig(proxyInfo: dict) -> dict: # load basic config option
config = {
'server': proxyInfo['server'],
'server_port': proxyInfo['port'], # type -> int
'method': proxyInfo['method'],
'password': proxyInfo['passwd'],
}
if proxyInfo['plugin'] is not None: # with plugin
config['plugin'] = proxyInfo['plugin']['type']
config['plugin_opts'] = proxyInfo['plugin']['param']
return config
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']:
return False # UDP is not used
if plugin in ['v2ray-plugin', 'xray-plugin', 'gost-plugin']:
if 'mode=quic' not in pluginParam.split(';'): # non-quic mode does not use UDP
return False
return True # UDP is assumed by default
def ssRust(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
config = loadConfig(proxyInfo)
if isUdp: # proxy UDP traffic
config['mode'] = 'tcp_and_udp'
return config, ['ss-rust-server', '-v']
def ssLibev(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
config = loadConfig(proxyInfo)
if isUdp: # proxy UDP traffic
config['mode'] = 'tcp_and_udp'
return config, ['ss-libev-server', '-v']
def ssPython(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
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:`
config['method'] = 'mbedtls:' + config['method']
if config['method'] in ['idea-cfb', 'seed-cfb']: # only older versions of openssl are supported
config['extra_opts'] = '--libopenssl=libcrypto.so.1.0.0'
if not isUdp:
config['no_udp'] = True # UDP traffic is not proxied
config['shadowsocks'] = 'ss-python-server'
return config, ['ss-bootstrap-server', '--debug', '-vv']
def ssPythonLegacy(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
config = loadConfig(proxyInfo)
if not isUdp:
config['no_udp'] = True # UDP traffic is not proxied
config['shadowsocks'] = 'ss-python-legacy-server'
return config, ['ss-bootstrap-server', '--debug', '-vv']
def loadPassword(method: str) -> str:
b64 = lambda x: base64.b64encode(x.encode(encoding = 'utf-8')).decode(encoding = 'utf-8')
if not method.startswith('2022-blake3-'):
return genFlag(length = 8)
if method == '2022-blake3-aes-128-gcm':
return b64(genFlag(length = 16))
return b64(genFlag(length = 32)) # three other 2022-blake3-* methods use 32 byte length password
def testConnection(serverType: str, clientType: str, method: str) -> dict:
proxyInfo = {
'server': settings['serverBind'],
'port': getAvailablePort(),
'method': method,
'passwd': loadPassword(method),
'plugin': None
}
socksInfo = {
'addr': settings['clientBind'],
'port': getAvailablePort()
}
ssClientLoad = {
'ss-rust': Shadowsocks.ssRust,
'ss-libev': Shadowsocks.ssLibev,
'ss-python': Shadowsocks.ssPython,
'ss-python-legacy': Shadowsocks.ssPythonLegacy
}[clientType]
ssConfig, ssClient = ssClientLoad(proxyInfo, socksInfo, isUdp = False)
clientFile = os.path.join(settings['workDir'], '%s_%s_%s' % (serverType, clientType, method) + '_client.json')
client = Process(settings['workDir'], cmd = ssClient + ['-c', clientFile], file = {
'path': clientFile,
'content': json.dumps(ssConfig)
}, isStart = False)
ssServerLoad = {
'ss-rust': ssRust,
'ss-libev': ssLibev,
'ss-python': ssPython,
'ss-python-legacy': ssPythonLegacy
}[serverType]
ssConfig, ssServer = ssServerLoad(proxyInfo, isUdp = False)
serverFile = os.path.join(settings['workDir'], '%s_%s_%s' % (serverType, clientType, method) + '_server.json')
server = Process(settings['workDir'], cmd = ssServer + ['-c', serverFile], file = {
'path': serverFile,
'content': json.dumps(ssConfig)
}, isStart = False)
testInfo = {
'title': 'Shadowsocks test: {%s <- %s -> %s}' % (serverType, method, clientType),
'socks': socksInfo,
'client': client,
'server': server,
}
logging.debug('New shadowsocks test connection -> %s' % testInfo)
return testInfo
def load(isExtra: bool = False) -> list:
result = []
if isExtra:
for ssServer in ssMethods:
for method in ssMethods[ssServer]:
for ssClient in ssMethods:
if method not in ssMethods[ssClient]: continue
result.append(testConnection(ssServer, ssClient, method))
else:
for method in ssAllMethods:
for ssType in ssMethods:
if method not in ssMethods[ssType]: continue
result.append(testConnection(ssType, ssType, method))
break
return result

167
test.py

@ -1,160 +1,53 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import json
import time
import requests
from Builder import Shadowsocks
from Tester import Shadowsocks
from Basis.Logger import logging
from Basis.Process import Process
from Basis.Methods import ssMethods
from Basis.Functions import getAvailablePort
ssPassword = ''
def loadConfig(serverPort: int, method: str) -> dict: # load basic config option
config = {
'server': '127.0.0.1',
'server_port': serverPort, # type -> int
'method': method,
'password': ssPassword,
}
return config
def test(testObj: dict) -> None:
logging.warning(testObj['title'])
testObj['client'].start()
testObj['server'].start()
time.sleep(1)
def ssRust(serverPort: int, method: str) -> tuple[dict, list]:
config = loadConfig(serverPort, method)
return config, ['ss-rust-server', '-v']
def ssLibev(serverPort: int, method: str) -> tuple[dict, list]:
config = loadConfig(serverPort, method)
return config, ['ss-libev-server', '-v']
def ssPython(serverPort: int, method: str) -> tuple[dict, list]:
config = loadConfig(serverPort, method)
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:`
config['method'] = 'mbedtls:' + config['method']
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['shadowsocks'] = 'ss-python-server'
return config, ['ss-bootstrap-server', '--debug', '-vv']
def ssPythonLegacy(serverPort: int, method: str) -> tuple[dict, list]:
config = loadConfig(serverPort, method)
config['shadowsocks'] = 'ss-python-legacy-server'
return config, ['ss-bootstrap-server', '--debug', '-vv']
def loadTest(serverType: str, clientType: str, method: str, timeout: float) -> None:
logging.warning('Shadowsocks test -> server = %s | client = %s | method = %s' % (serverType, clientType, method))
global ssPassword
if method.startswith('2022-blake3-'):
ssPassword = 'ZG5vbWQzNDNkbm9tZDM0M2Rub21kMzQzZG5vbWQzNDM=' # base64 encode of 'dnomd343' * 4
if method == '2022-blake3-aes-128-gcm':
ssPassword = 'ZG5vbWQzNDNkbm9tZDM0Mw==' # base64 encode of 'dnomd343' * 2
else:
ssPassword = 'dnomd343'
title = '%s_%s_%s' % (serverType, clientType, method)
workDir = '/tmp/ProxyC'
serverPort = getAvailablePort()
socksPort = getAvailablePort()
proxyInfo = {
'server': '127.0.0.1',
'port': serverPort,
'method': method,
'passwd': ssPassword,
'plugin': None
}
socksInfo = {
'addr': '127.0.0.1',
'port': socksPort
}
ssClientLoad = {
'ss-rust': Shadowsocks.ssRust,
'ss-libev': Shadowsocks.ssLibev,
'ss-python': Shadowsocks.ssPython,
'ss-python-legacy': Shadowsocks.ssPythonLegacy
}[clientType]
ssConfig, ssClient = ssClientLoad(proxyInfo, socksInfo, isUdp = False)
client = Process(workDir, cmd = ssClient + ['-c', os.path.join(workDir, title + '_client.json')], file = {
'path': os.path.join(workDir, title + '_client.json'),
'content': json.dumps(ssConfig)
}, isStart = False)
ssServerLoad = {
'ss-rust': ssRust,
'ss-libev': ssLibev,
'ss-python': ssPython,
'ss-python-legacy': ssPythonLegacy
}[serverType]
ssConfig, ssServer = ssServerLoad(serverPort, method)
server = Process(workDir, cmd = ssServer + ['-c', os.path.join(workDir, title + '_server.json')], file = {
'path': os.path.join(workDir, title + '_server.json'),
'content': json.dumps(ssConfig)
}, isStart = False)
client.start()
server.start()
time.sleep(timeout)
errFlag = False
try:
request = requests.get(
'http://baidu.com',
proxies = {
'http': 'socks5://127.0.0.1:%i' % socksPort,
'https': 'socks5://127.0.0.1:%i' % socksPort
'http': 'socks5://127.0.0.1:%i' % testObj['socks']['port'],
'https': 'socks5://127.0.0.1:%i' % testObj['socks']['port'],
},
timeout = 10
)
request.raise_for_status()
logging.info('socks5 127.0.0.1:%i -> ok' % socksPort)
logging.info('socks5 127.0.0.1:%i -> ok' % testObj['socks']['port'])
except Exception as exp:
logging.error('socks5 127.0.0.1:%i -> error' % socksPort)
logging.error('socks5 127.0.0.1:%i -> error' % testObj['socks']['port'])
logging.error('requests exception\n' + str(exp))
errFlag = True
client.quit()
server.quit()
if errFlag:
logging.error('client capture output\n' + str(client.output))
logging.error('server capture output\n' + str(server.output))
def test_1() -> None:
for ssType in ssMethods:
for method in ssMethods[ssType]:
timeout = 0.1
if 'python' in ssType: timeout = 0.3
if 'python-legacy' in ssType:
timeout = 0.8
if method == 'table' or method == 'salsa20-ctr': timeout = 2
loadTest(ssType, ssType, method, timeout)
def test_2() -> None:
for ssServer in ssMethods:
for method in ssMethods[ssServer]:
for ssClient in ssMethods:
if method not in ssMethods[ssClient]: continue
timeout = 0.1
if 'python' in ssServer or 'python' in ssClient: timeout = 0.3
if method == 'table': timeout = 0.8
if 'python-legacy' in ssServer or 'python-legacy' in ssClient: timeout = 1
if method == 'salsa20-ctr': timeout = 3
loadTest(ssServer, ssClient, method, timeout)
# test_1()
test_2()
# loadTest('ss-python-legacy', 'ss-python-legacy', 'salsa20-ctr', 3)
testObj['client'].quit()
testObj['server'].quit()
if errFlag:
logging.warning('client info')
logging.error('command -> ' + str(testObj['client'].cmd))
logging.error('envVar -> ' + str(testObj['client'].env))
logging.error('file -> ' + str(testObj['client'].file))
logging.warning('client capture output')
logging.error('\n' + str(testObj['client'].output))
logging.warning('server info')
logging.error('command -> ' + str(testObj['server'].cmd))
logging.error('envVar -> ' + str(testObj['server'].env))
logging.error('file -> ' + str(testObj['server'].file))
logging.warning('server capture output')
logging.error('\n' + str(testObj['server'].output))
testList = Shadowsocks.load(isExtra = True)
for testObject in testList:
test(testObject)

Loading…
Cancel
Save