mirror of
https://github.com/klzgrad/naiveproxy.git
synced 2026-10-24 14:03:22 +03:00
Improve tests
This commit is contained in:
+385
-311
@@ -1,340 +1,414 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise naive's configuration, authentication, and proxy chaining.
|
||||
|
||||
Run with --naive=/path/to/naive and --server_protocol=https or http.
|
||||
Four cases require port 1080; use --skip-default-port-tests if it is occupied.
|
||||
The existing --rootfs and --target_cpu options support bwrap and qemu-user.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections import deque
|
||||
import contextlib
|
||||
import http.server
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import queue
|
||||
import shlex
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--naive', required=True)
|
||||
parser.add_argument('--rootfs')
|
||||
parser.add_argument('--target_cpu')
|
||||
parser.add_argument('--server_protocol',
|
||||
choices=['http', 'https'], default='https')
|
||||
argv = parser.parse_args()
|
||||
|
||||
if argv.rootfs:
|
||||
try:
|
||||
os.remove(os.path.join(argv.rootfs, 'naive'))
|
||||
except OSError:
|
||||
HOST = '127.0.0.1'
|
||||
PAYLOAD = b'Naive basic integration test\n' * 128
|
||||
QEMU_CPUS = {
|
||||
'arm64': 'aarch64',
|
||||
'arm': 'arm',
|
||||
'mipsel': 'mipsel',
|
||||
'mips64el': 'mips64el',
|
||||
'riscv64': 'riscv64',
|
||||
'loong64': 'loong64',
|
||||
}
|
||||
|
||||
|
||||
class TestFailure(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ListenFailure(TestFailure):
|
||||
pass
|
||||
|
||||
|
||||
class OriginHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Length', str(len(PAYLOAD)))
|
||||
self.end_headers()
|
||||
self.wfile.write(PAYLOAD)
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
server_protocol = argv.server_protocol
|
||||
|
||||
_, certfile = tempfile.mkstemp()
|
||||
|
||||
result = subprocess.run(
|
||||
f'openssl req -new -x509 -keyout {certfile} -out {certfile} -days 1 -nodes -subj /C=XX'.split(), capture_output=True)
|
||||
result.check_returncode()
|
||||
|
||||
HTTPS_SERVER_HOSTNAME = '127.0.0.1'
|
||||
HTTP_SERVER_PORT = 60443 if server_protocol == 'https' else 60080
|
||||
|
||||
httpd = http.server.HTTPServer(
|
||||
(HTTPS_SERVER_HOSTNAME, HTTP_SERVER_PORT), http.server.SimpleHTTPRequestHandler)
|
||||
httpd.timeout = 1
|
||||
httpd.allow_reuse_address = True
|
||||
if server_protocol == 'https':
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.load_cert_chain(certfile=certfile)
|
||||
httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side=True)
|
||||
|
||||
httpd_thread = threading.Thread(
|
||||
target=lambda httpd: httpd.serve_forever(), args=(httpd,), daemon=True)
|
||||
httpd_thread.start()
|
||||
@contextlib.contextmanager
|
||||
def origin_server(protocol, directory):
|
||||
with http.server.ThreadingHTTPServer((HOST, 0), OriginHandler) as server:
|
||||
if protocol == 'https':
|
||||
certfile = directory / 'origin.pem'
|
||||
# A P-256 key is quick to generate. This certificate belongs to
|
||||
# curl's destination; naive only relays the encrypted bytes.
|
||||
result = subprocess.run(
|
||||
['openssl', 'req', '-new', '-x509', '-newkey', 'ec',
|
||||
'-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes',
|
||||
'-keyout', str(certfile), '-out', str(certfile),
|
||||
'-days', '1', '-subj', '/CN=localhost'],
|
||||
capture_output=True, timeout=10, text=True)
|
||||
if result.returncode:
|
||||
raise TestFailure(f'Certificate generation failed:\n{result.stderr}')
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(certfile)
|
||||
server.socket = context.wrap_socket(server.socket, server_side=True)
|
||||
thread = threading.Thread(
|
||||
target=server.serve_forever, kwargs={'poll_interval': 0.05}, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
yield server.server_port
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join()
|
||||
|
||||
|
||||
def test_https_server(hostname, port, proxy=None):
|
||||
url = f'{server_protocol}://{hostname}:{port}/404'
|
||||
cmdline = ['curl', '-k', '-s']
|
||||
if proxy:
|
||||
cmdline.extend(['--proxy', proxy])
|
||||
cmdline.append(url)
|
||||
print('subprocess.run', ' '.join(cmdline))
|
||||
result = subprocess.run(cmdline, capture_output=True,
|
||||
timeout=10, text=True, encoding='utf-8')
|
||||
print(result.stderr, end='')
|
||||
return 'Error code: 404' in result.stdout
|
||||
class BasicTests:
|
||||
def __init__(self, options, directory, origin_port):
|
||||
self.options = options
|
||||
self.directory = directory
|
||||
self.origin_port = origin_port
|
||||
self.passed = 0
|
||||
self.skipped = 0
|
||||
|
||||
def request(self, proxy=None):
|
||||
url = f'{self.options.server_protocol}://{HOST}:{self.origin_port}/'
|
||||
# Ignore ~/.curlrc, NO_PROXY, and inherited proxy settings. Every
|
||||
# proxied case must actually traverse the client under test.
|
||||
command = ['curl', '--disable', '--insecure', '--silent', '--show-error',
|
||||
'--fail', '--max-time', '10', '--noproxy', '',
|
||||
'--proxy', proxy or '', url]
|
||||
result = subprocess.run(command, capture_output=True, timeout=15)
|
||||
if result.returncode or result.stdout != PAYLOAD:
|
||||
raise TestFailure(
|
||||
f'{shlex.join(command)} failed (exit {result.returncode}):\n'
|
||||
f'{result.stderr.decode(errors="replace")}\n'
|
||||
f'Received {len(result.stdout)} bytes; expected {len(PAYLOAD)}')
|
||||
|
||||
assert test_https_server(HTTPS_SERVER_HOSTNAME,
|
||||
HTTP_SERVER_PORT), 'https server not up'
|
||||
|
||||
|
||||
def start_naive(naive_args, config_file):
|
||||
with_qemu = None
|
||||
if argv.target_cpu == 'arm64':
|
||||
with_qemu = 'aarch64'
|
||||
elif argv.target_cpu == 'arm':
|
||||
with_qemu = 'arm'
|
||||
elif argv.target_cpu == 'mipsel':
|
||||
with_qemu = 'mipsel'
|
||||
elif argv.target_cpu == 'mips64el':
|
||||
with_qemu = 'mips64el'
|
||||
elif argv.target_cpu == 'riscv64':
|
||||
with_qemu = 'riscv64'
|
||||
elif argv.target_cpu == 'loong64':
|
||||
with_qemu = 'loong64'
|
||||
|
||||
if argv.rootfs:
|
||||
if not with_qemu:
|
||||
if not os.path.exists(os.path.join(argv.rootfs, 'naive')):
|
||||
shutil.copy2(argv.naive, argv.rootfs)
|
||||
# bwrap isolates filesystem, config_file needs a copy inside.
|
||||
if config_file is not None:
|
||||
shutil.copy2(config_file, argv.rootfs)
|
||||
cmdline = ['bwrap', '--die-with-parent', '--bind', argv.rootfs, '/',
|
||||
'--proc', '/proc', '--dev', '/dev', '--chdir', '/', '/naive']
|
||||
@contextlib.contextmanager
|
||||
def start_naive(self, arguments):
|
||||
qemu = QEMU_CPUS.get(self.options.target_cpu)
|
||||
if self.options.rootfs:
|
||||
if qemu:
|
||||
command = [f'qemu-{qemu}', '-L', self.options.rootfs,
|
||||
self.options.naive]
|
||||
else:
|
||||
# Mount the private test directory and binary rather than
|
||||
# copying config files or replacing /naive in the rootfs.
|
||||
(self.directory / 'naive').touch(exist_ok=True)
|
||||
command = ['bwrap', '--die-with-parent',
|
||||
'--bind', self.options.rootfs, '/',
|
||||
'--bind', str(self.directory), '/tmp',
|
||||
'--ro-bind', self.options.naive, '/tmp/naive',
|
||||
'--proc', '/proc', '--dev', '/dev',
|
||||
'--chdir', '/tmp', '/tmp/naive']
|
||||
else:
|
||||
cmdline = [f'qemu-{with_qemu}',
|
||||
'-L', argv.rootfs, argv.naive]
|
||||
else:
|
||||
cmdline = [argv.naive]
|
||||
cmdline.extend(naive_args)
|
||||
command = [self.options.naive]
|
||||
command.extend(arguments)
|
||||
env = os.environ.copy()
|
||||
env['TEST_MARK_STARTUP'] = 'yes'
|
||||
proc = subprocess.Popen(command, cwd=self.directory, env=env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
|
||||
text=True, encoding='utf-8', errors='replace')
|
||||
lines = deque(maxlen=2000)
|
||||
startup = queue.Queue()
|
||||
|
||||
env = os.environ.copy()
|
||||
env["TEST_MARK_STARTUP"] = "yes"
|
||||
proc = subprocess.Popen(cmdline, stdout=subprocess.DEVNULL, env=env,
|
||||
stderr=subprocess.PIPE, text=True, encoding='utf-8')
|
||||
print('subprocess.Popen', ' '.join(cmdline), 'pid:', proc.pid)
|
||||
def drain_stderr():
|
||||
try:
|
||||
for line in proc.stderr:
|
||||
lines.append(line)
|
||||
if 'Failed to listen' in line or 'TEST_MARK_STARTUP' in line:
|
||||
startup.put(line)
|
||||
finally:
|
||||
startup.put(None)
|
||||
|
||||
def terminate(proc):
|
||||
print('proc has timed out')
|
||||
print('terminate pid', proc.pid)
|
||||
proc.terminate()
|
||||
reader = threading.Thread(target=drain_stderr, daemon=True)
|
||||
reader.start()
|
||||
failed = False
|
||||
try:
|
||||
# Keep the existing allowance for slow qemu-user startup.
|
||||
deadline = time.monotonic() + 20
|
||||
while True:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TestFailure('naive did not start within 20 seconds')
|
||||
try:
|
||||
line = startup.get(timeout=remaining)
|
||||
except queue.Empty:
|
||||
raise TestFailure('naive did not start within 20 seconds') from None
|
||||
if line is None:
|
||||
raise TestFailure(f'naive exited before startup (exit {proc.wait()})')
|
||||
if 'Failed to listen' in line:
|
||||
raise ListenFailure(line.strip())
|
||||
if 'TEST_MARK_STARTUP' in line:
|
||||
break
|
||||
yield proc
|
||||
except BaseException:
|
||||
failed = True
|
||||
raise
|
||||
finally:
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
reader.join()
|
||||
proc.stderr.close()
|
||||
if failed:
|
||||
print(f'Client log: {shlex.join(command)}', file=sys.stderr)
|
||||
print(''.join(lines), file=sys.stderr, end='')
|
||||
|
||||
# Some qemu-user tests take a while to start.
|
||||
# See https://gitlab.com/qemu-project/qemu/-/issues/1729
|
||||
timeout = threading.Timer(20, terminate, args=(proc,))
|
||||
timeout.start()
|
||||
while True:
|
||||
if proc.poll() is not None:
|
||||
timeout.cancel()
|
||||
return proc.poll() == 0
|
||||
def test_naive_once(self, proxy, *arguments, **kwargs):
|
||||
allocated = set()
|
||||
|
||||
line = proc.stderr.readline().strip()
|
||||
print(line)
|
||||
if 'Failed to listen' in line:
|
||||
timeout.cancel()
|
||||
print('terminate pid', proc.pid)
|
||||
proc.terminate()
|
||||
return 'Failed to listen'
|
||||
elif 'TEST_MARK_STARTUP' in line:
|
||||
timeout.cancel()
|
||||
return proc
|
||||
class PortDict(dict):
|
||||
def __missing__(self, key):
|
||||
if not key.startswith('PORT'):
|
||||
return key
|
||||
# Binding without port reuse detects occupied listeners,
|
||||
# including naive listeners that enable SO_REUSEPORT.
|
||||
while True:
|
||||
with socket.socket() as sock:
|
||||
sock.bind((HOST, 0))
|
||||
port = sock.getsockname()[1]
|
||||
if port >= 1024 and port not in allocated:
|
||||
break
|
||||
allocated.add(port)
|
||||
self[key] = str(port)
|
||||
return self[key]
|
||||
|
||||
ports = PortDict()
|
||||
proxy = proxy.format_map(ports)
|
||||
config_file = kwargs.get('config_file')
|
||||
config_content = kwargs.get('config_content')
|
||||
config_path = self.directory / config_file if config_file else None
|
||||
try:
|
||||
if config_content is not None:
|
||||
config_path.write_text('{' + config_content.format_map(ports) + '}')
|
||||
with contextlib.ExitStack() as clients:
|
||||
processes = []
|
||||
for arguments_instance in arguments:
|
||||
naive_arguments = shlex.split(arguments_instance.format_map(ports))
|
||||
processes.append(clients.enter_context(self.start_naive(naive_arguments)))
|
||||
self.request(proxy)
|
||||
if any(proc.poll() is not None for proc in processes):
|
||||
raise TestFailure('A client exited during the request')
|
||||
finally:
|
||||
if config_path is not None:
|
||||
config_path.unlink(missing_ok=True)
|
||||
|
||||
def test_naive(self, label, proxy, *arguments, **kwargs):
|
||||
reason = None
|
||||
if self.options.target_cpu == 'arm' and not label.startswith('Default'):
|
||||
# Preserve the existing skips for slow arm qemu-user tests.
|
||||
reason = 'slow arm qemu-user test'
|
||||
elif kwargs.get('uses_default_port'):
|
||||
if self.options.skip_default_port_tests:
|
||||
reason = '--skip-default-port-tests'
|
||||
else:
|
||||
with socket.socket() as sock:
|
||||
# Allow old connections in TIME_WAIT, but do not enable
|
||||
# SO_REUSEPORT, which would hide an active listener.
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind(('0.0.0.0', 1080))
|
||||
except OSError:
|
||||
raise TestFailure(
|
||||
f'{label}: port 1080 is occupied; use '
|
||||
'--skip-default-port-tests to omit the four cases '
|
||||
'that require the default port') from None
|
||||
if reason:
|
||||
self.skipped += 1
|
||||
print(f'** SKIP TEST: {label} ({reason})', flush=True)
|
||||
return
|
||||
|
||||
for attempt in range(5):
|
||||
try:
|
||||
self.test_naive_once(proxy, *arguments, **kwargs)
|
||||
break
|
||||
except ListenFailure:
|
||||
if attempt == 4:
|
||||
raise
|
||||
# Cleanup has reaped every client. Retry with fresh ports;
|
||||
# there is no reason to sleep for a fixed second.
|
||||
print(f'Retrying {label} with new ports...', flush=True)
|
||||
except TestFailure as error:
|
||||
raise TestFailure(f'{label}: {error}') from error
|
||||
self.passed += 1
|
||||
print(f'** TEST PASS: {label}', flush=True)
|
||||
|
||||
|
||||
port = 10000
|
||||
def run_tests(test_naive):
|
||||
test_naive('Default config', 'socks5h://127.0.0.1:1080',
|
||||
'--log', uses_default_port=True)
|
||||
|
||||
test_naive('Default config file', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'',
|
||||
config_content='"listen":"socks://127.0.0.1:{PORT1}","log":""',
|
||||
config_file='config.json')
|
||||
|
||||
test_naive('Custom config file', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'custom.json',
|
||||
config_content='"listen":"socks://127.0.0.1:{PORT1}","log":""',
|
||||
config_file='custom.json')
|
||||
|
||||
test_naive('Multiple listens - command line', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --listen=http://:{PORT2}')
|
||||
|
||||
test_naive('Multiple listens - command line', 'http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT1} --listen=http://:{PORT2}')
|
||||
|
||||
test_naive('Multiple listens - config file', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'multiple-listen.json',
|
||||
config_content='"listen":["socks://:{PORT1}", "http://:{PORT2}"],"log":""',
|
||||
config_file='multiple-listen.json')
|
||||
|
||||
test_naive('Multiple listens - config file', 'http://127.0.0.1:{PORT2}',
|
||||
'multiple-listen.json',
|
||||
config_content='"listen":["socks://:{PORT1}", "http://:{PORT2}"],"log":""',
|
||||
config_file='multiple-listen.json')
|
||||
|
||||
test_naive('Multiple proxies - command line', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --listen=socks://:{PORT2} --proxy=http://127.0.0.1:{PORT3} --proxy=http://127.0.0.1:{PORT4}',
|
||||
'--log --listen=http://:{PORT3} --listen=http://:{PORT4} --proxy=socks://127.0.0.1:{PORT5}',
|
||||
'--log --listen=socks://:{PORT5}')
|
||||
|
||||
test_naive('Multiple proxies - command line', 'socks5h://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT1} --listen=socks://:{PORT2} --proxy=http://127.0.0.1:{PORT3} --proxy=http://127.0.0.1:{PORT4}',
|
||||
'--log --listen=http://:{PORT3} --listen=http://:{PORT4} --proxy=socks://127.0.0.1:{PORT5}',
|
||||
'--log --listen=socks://:{PORT5}')
|
||||
|
||||
test_naive('Multiple proxies - different auth', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --listen=socks://:{PORT2} --proxy=http://user1:pass1@127.0.0.1:{PORT3} --proxy=http://user2:pass2@127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://user1:pass1@127.0.0.1:{PORT3}')
|
||||
|
||||
test_naive('Multiple proxies - different auth', 'socks5h://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT1} --listen=socks://:{PORT2} --proxy=http://user1:pass1@127.0.0.1:{PORT3} --proxy=http://user2:pass2@127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://user2:pass2@127.0.0.1:{PORT3}')
|
||||
|
||||
test_naive('Trivial - listen scheme only', 'socks5h://127.0.0.1:1080',
|
||||
'--log --listen=socks://', uses_default_port=True)
|
||||
|
||||
test_naive('Trivial - listen no host', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1}')
|
||||
|
||||
test_naive('Trivial - listen no port', 'socks5h://127.0.0.1:1080',
|
||||
'--log --listen=socks://127.0.0.1', uses_default_port=True)
|
||||
|
||||
test_naive('Trivial - auth', 'socks5h://user:pass@127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://user:pass@127.0.0.1:{PORT1}')
|
||||
|
||||
test_naive('Trivial - auth with special chars', 'socks5h://user:^@127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://user:^@127.0.0.1:{PORT1}')
|
||||
|
||||
test_naive('Trivial - auth with special chars', 'socks5h://^:^@127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://^:^@127.0.0.1:{PORT1}')
|
||||
|
||||
test_naive('Trivial - auth with empty pass', 'socks5h://user:@127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://user:@127.0.0.1:{PORT1}')
|
||||
|
||||
test_naive('SOCKS-SOCKS', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=socks://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT2}')
|
||||
|
||||
test_naive('SOCKS-SOCKS - proxy no port', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=socks://127.0.0.1',
|
||||
'--log --listen=socks://:1080', uses_default_port=True)
|
||||
|
||||
test_naive('SOCKS-HTTP', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://:{PORT2}')
|
||||
|
||||
test_naive('HTTP-HTTP', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://:{PORT2}')
|
||||
|
||||
test_naive('HTTP-SOCKS', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=socks://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT2}')
|
||||
|
||||
test_naive('SOCKS-SOCKS-SOCKS', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=socks://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT2} --proxy=socks://127.0.0.1:{PORT3}',
|
||||
'--log --listen=socks://:{PORT3}')
|
||||
|
||||
test_naive('SOCKS-HTTP-SOCKS', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://:{PORT2} --proxy=socks://127.0.0.1:{PORT3}',
|
||||
'--log --listen=socks://:{PORT3}')
|
||||
|
||||
test_naive('HTTP-SOCKS-HTTP', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=socks://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT2} --proxy=http://127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://:{PORT3}')
|
||||
|
||||
test_naive('HTTP-HTTP-HTTP', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://:{PORT2} --proxy=http://127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://:{PORT3}')
|
||||
|
||||
test_naive('HTTP-HTTP (with auth)', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=http://hello:world@127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://hello:world@127.0.0.1:{PORT2}')
|
||||
|
||||
test_naive('HTTPa-HTTPb,HTTPc (chaining with remote loop)', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT2}',
|
||||
'--log --listen=http://:{PORT1} --proxy=http://127.0.0.1:{PORT2},http://127.0.0.1:{PORT2}')
|
||||
|
||||
test_naive('HTTPa-HTTPb,HTTPc (chaining with multiple auth)', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://hello:world2@127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://hello:world3@127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://127.0.0.1:{PORT1} --proxy=http://hello:world2@127.0.0.1:{PORT2},http://hello:world3@127.0.0.1:{PORT3}')
|
||||
|
||||
|
||||
def allocate_port_number():
|
||||
global port
|
||||
port += 1
|
||||
if port > 60000:
|
||||
port = 10000
|
||||
return port
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--naive', required=True)
|
||||
parser.add_argument('--rootfs')
|
||||
parser.add_argument('--target_cpu')
|
||||
parser.add_argument('--server_protocol', choices=['http', 'https'], default='https')
|
||||
parser.add_argument('--skip-default-port-tests', action='store_true',
|
||||
help='skip the four cases that require port 1080')
|
||||
options = parser.parse_args()
|
||||
options.naive = str(Path(options.naive).resolve())
|
||||
if options.rootfs:
|
||||
options.rootfs = str(Path(options.rootfs).resolve())
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with tempfile.TemporaryDirectory(prefix='naive-basic-') as tmp:
|
||||
directory = Path(tmp)
|
||||
with origin_server(options.server_protocol, directory) as origin_port:
|
||||
tests = BasicTests(options, directory, origin_port)
|
||||
tests.request()
|
||||
run_tests(tests.test_naive)
|
||||
print(f'{options.server_protocol}: {tests.passed} passed, '
|
||||
f'{tests.skipped} skipped in {time.monotonic() - started:.2f}s')
|
||||
return 0
|
||||
except (TestFailure, OSError, subprocess.TimeoutExpired) as error:
|
||||
print(f'** TEST FAIL: {error}', file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def test_naive_once(proxy, *args, **kwargs):
|
||||
port_map = {}
|
||||
|
||||
class PortDict(dict):
|
||||
def __init__(self, port_map):
|
||||
self._port_map = port_map
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key.startswith('PORT'):
|
||||
if key not in self._port_map:
|
||||
self._port_map[key] = str(allocate_port_number())
|
||||
return self._port_map[key]
|
||||
return key
|
||||
port_dict = PortDict(port_map)
|
||||
|
||||
proxy = proxy.format_map(port_dict)
|
||||
|
||||
config_file = kwargs.get('config_file')
|
||||
config_content = kwargs.get('config_content')
|
||||
if config_content is not None:
|
||||
config_content = config_content.format_map(port_dict)
|
||||
print(f"Writing {repr(config_content)} to {config_file}")
|
||||
with open(config_file, 'w') as f:
|
||||
f.write('{')
|
||||
f.write(config_content)
|
||||
f.write('}')
|
||||
|
||||
naive_procs = []
|
||||
|
||||
def cleanup():
|
||||
if config_file is not None:
|
||||
os.remove(config_file)
|
||||
for naive_proc in naive_procs:
|
||||
print('terminate pid', naive_proc.pid)
|
||||
naive_proc.terminate()
|
||||
|
||||
for args_instance in args:
|
||||
naive_args = args_instance.format_map(port_dict).split()
|
||||
naive_proc = start_naive(naive_args, config_file)
|
||||
if naive_proc == 'Failed to listen':
|
||||
cleanup()
|
||||
return 'Failed to listen'
|
||||
if not naive_proc:
|
||||
cleanup()
|
||||
return False
|
||||
naive_procs.append(naive_proc)
|
||||
|
||||
result = test_https_server(HTTPS_SERVER_HOSTNAME, HTTP_SERVER_PORT, proxy)
|
||||
|
||||
cleanup()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def test_naive(label, proxy, *args, **kwargs):
|
||||
RETRIES = 5
|
||||
result = None
|
||||
if argv.target_cpu == 'arm' and not label.startswith('Default'):
|
||||
# Arm tests are too slow in qemu-user
|
||||
# due to https://www.openwall.com/lists/musl/2017/06/15/9
|
||||
print('** SKIP TEST:', label, end='\n\n')
|
||||
return
|
||||
|
||||
for i in range(RETRIES):
|
||||
result = test_naive_once(proxy, *args, **kwargs)
|
||||
if result == 'Failed to listen':
|
||||
result = False
|
||||
print('Retrying...')
|
||||
time.sleep(1)
|
||||
continue
|
||||
break
|
||||
if result is True:
|
||||
print('** TEST PASS:', label, end='\n\n')
|
||||
else:
|
||||
print('** TEST FAIL:', label, end='\n\n')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
test_naive('Default config', 'socks5h://127.0.0.1:1080',
|
||||
'--log')
|
||||
|
||||
test_naive('Default config file', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'',
|
||||
config_content='"listen":"socks://127.0.0.1:{PORT1}","log":""',
|
||||
config_file='config.json')
|
||||
|
||||
test_naive('Custom config file', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'custom.json',
|
||||
config_content='"listen":"socks://127.0.0.1:{PORT1}","log":""',
|
||||
config_file='custom.json')
|
||||
|
||||
test_naive('Multiple listens - command line', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --listen=http://:{PORT2}')
|
||||
|
||||
test_naive('Multiple listens - command line', 'http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT1} --listen=http://:{PORT2}')
|
||||
|
||||
test_naive('Multiple listens - config file', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'multiple-listen.json',
|
||||
config_content='"listen":["socks://:{PORT1}", "http://:{PORT2}"],"log":""',
|
||||
config_file='multiple-listen.json')
|
||||
|
||||
test_naive('Multiple listens - config file', 'http://127.0.0.1:{PORT2}',
|
||||
'multiple-listen.json',
|
||||
config_content='"listen":["socks://:{PORT1}", "http://:{PORT2}"],"log":""',
|
||||
config_file='multiple-listen.json')
|
||||
|
||||
test_naive('Multiple proxies - command line', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --listen=socks://:{PORT2} --proxy=http://127.0.0.1:{PORT3} --proxy=http://127.0.0.1:{PORT4}',
|
||||
'--log --listen=http://:{PORT3} --listen=http://:{PORT4} --proxy=socks://127.0.0.1:{PORT5}',
|
||||
'--log --listen=socks://:{PORT5}')
|
||||
|
||||
test_naive('Multiple proxies - command line', 'socks5h://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT1} --listen=socks://:{PORT2} --proxy=http://127.0.0.1:{PORT3} --proxy=http://127.0.0.1:{PORT4}',
|
||||
'--log --listen=http://:{PORT3} --listen=http://:{PORT4} --proxy=socks://127.0.0.1:{PORT5}',
|
||||
'--log --listen=socks://:{PORT5}')
|
||||
|
||||
test_naive('Multiple proxies - different auth', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --listen=socks://:{PORT2} --proxy=http://user1:pass1@127.0.0.1:{PORT3} --proxy=http://user2:pass2@127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://user1:pass1@127.0.0.1:{PORT3}')
|
||||
|
||||
test_naive('Multiple proxies - different auth', 'socks5h://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT1} --listen=socks://:{PORT2} --proxy=http://user1:pass1@127.0.0.1:{PORT3} --proxy=http://user2:pass2@127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://user2:pass2@127.0.0.1:{PORT3}')
|
||||
|
||||
test_naive('Trivial - listen scheme only', 'socks5h://127.0.0.1:1080',
|
||||
'--log --listen=socks://')
|
||||
|
||||
test_naive('Trivial - listen no host', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1}')
|
||||
|
||||
test_naive('Trivial - listen no port', 'socks5h://127.0.0.1:1080',
|
||||
'--log --listen=socks://127.0.0.1')
|
||||
|
||||
test_naive('Trivial - auth', 'socks5h://user:pass@127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://user:pass@127.0.0.1:{PORT1}')
|
||||
|
||||
test_naive('Trivial - auth with special chars', 'socks5h://user:^@127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://user:^@127.0.0.1:{PORT1}')
|
||||
|
||||
test_naive('Trivial - auth with special chars', 'socks5h://^:^@127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://^:^@127.0.0.1:{PORT1}')
|
||||
|
||||
test_naive('Trivial - auth with empty pass', 'socks5h://user:@127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://user:@127.0.0.1:{PORT1}')
|
||||
|
||||
test_naive('SOCKS-SOCKS', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=socks://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT2}')
|
||||
|
||||
test_naive('SOCKS-SOCKS - proxy no port', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=socks://127.0.0.1',
|
||||
'--log --listen=socks://:1080')
|
||||
|
||||
test_naive('SOCKS-HTTP', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://:{PORT2}')
|
||||
|
||||
test_naive('HTTP-HTTP', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://:{PORT2}')
|
||||
|
||||
test_naive('HTTP-SOCKS', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=socks://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT2}')
|
||||
|
||||
test_naive('SOCKS-SOCKS-SOCKS', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=socks://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT2} --proxy=socks://127.0.0.1:{PORT3}',
|
||||
'--log --listen=socks://:{PORT3}')
|
||||
|
||||
test_naive('SOCKS-HTTP-SOCKS', 'socks5h://127.0.0.1:{PORT1}',
|
||||
'--log --listen=socks://:{PORT1} --proxy=http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://:{PORT2} --proxy=socks://127.0.0.1:{PORT3}',
|
||||
'--log --listen=socks://:{PORT3}')
|
||||
|
||||
test_naive('HTTP-SOCKS-HTTP', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=socks://127.0.0.1:{PORT2}',
|
||||
'--log --listen=socks://:{PORT2} --proxy=http://127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://:{PORT3}')
|
||||
|
||||
test_naive('HTTP-HTTP-HTTP', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=http://127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://:{PORT2} --proxy=http://127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://:{PORT3}')
|
||||
|
||||
test_naive('HTTP-HTTP (with auth)', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT1} --proxy=http://hello:world@127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://hello:world@127.0.0.1:{PORT2}')
|
||||
|
||||
test_naive('HTTPa-HTTPb,HTTPc (chaining with remote loop)', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://:{PORT2}',
|
||||
'--log --listen=http://:{PORT1} --proxy=http://127.0.0.1:{PORT2},http://127.0.0.1:{PORT2}')
|
||||
|
||||
test_naive('HTTPa-HTTPb,HTTPc (chaining with multiple auth)', 'http://127.0.0.1:{PORT1}',
|
||||
'--log --listen=http://hello:world2@127.0.0.1:{PORT2}',
|
||||
'--log --listen=http://hello:world3@127.0.0.1:{PORT3}',
|
||||
'--log --listen=http://127.0.0.1:{PORT1} --proxy=http://hello:world2@127.0.0.1:{PORT2},http://hello:world3@127.0.0.1:{PORT3}')
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test SSL_CERT_FILE and SSL_CERT_DIR with HTTPS and QUIC (no root required).
|
||||
|
||||
Usage: python3 tests/ssl_cert_file.py --naive=src/out/Release/naive
|
||||
Requires openssl, curl, and Caddy with the naive forwardproxy plugin. Caddy
|
||||
defaults to tests/caddy; use --caddy to select another binary. All traffic is
|
||||
local, and certificates, configs, and logs live in a temporary directory.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
TESTS_DIR = Path(__file__).resolve().parent
|
||||
CERTS_DIR = TESTS_DIR.parent / 'src/net/tools/quic/certs'
|
||||
PAYLOAD = b'Local CA integration test\n' * 1024
|
||||
|
||||
|
||||
def run(command, **kwargs):
|
||||
result = subprocess.run(command, capture_output=True, timeout=30, **kwargs)
|
||||
if result.returncode:
|
||||
raise RuntimeError(
|
||||
f'{command} exited with {result.returncode}:\n'
|
||||
f'{result.stderr.decode(errors="replace")}')
|
||||
return result
|
||||
|
||||
|
||||
def generate_certs(directory):
|
||||
# The Chromium script uses relative paths and removes ./out. Copy it and
|
||||
# its configs so it runs exclusively inside our temporary directory.
|
||||
directory.mkdir()
|
||||
for name in ('generate-certs.sh', 'ca.cnf', 'leaf.cnf'):
|
||||
shutil.copyfile(CERTS_DIR / name, directory / name)
|
||||
env = os.environ.copy()
|
||||
# These variables override the defaults in Chromium's OpenSSL configs.
|
||||
for name in ('CA_DIR', 'KEY_SIZE', 'ALGO', 'CERT_TYPE', 'CA_NAME',
|
||||
'SUBJECT_NAME'):
|
||||
env.pop(name, None)
|
||||
run(['sh', 'generate-certs.sh'], cwd=directory, env=env)
|
||||
return directory / 'out'
|
||||
|
||||
|
||||
def unused_port(udp=False):
|
||||
# QUIC needs a port available for both TCP and UDP. Hold both sockets
|
||||
# while checking; the short gap before Caddy binds is unavoidable.
|
||||
for _ in range(100):
|
||||
with socket.socket() as tcp:
|
||||
tcp.bind(('127.0.0.1', 0))
|
||||
port = tcp.getsockname()[1]
|
||||
if port < 1024:
|
||||
continue
|
||||
if udp:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
try:
|
||||
sock.bind(('127.0.0.1', port))
|
||||
except OSError:
|
||||
continue
|
||||
return port
|
||||
raise RuntimeError('Could not allocate an unprivileged loopback port')
|
||||
|
||||
|
||||
def netlog_events(netlog):
|
||||
# Naive exits on SIGTERM without closing the NetLog JSON document. Each
|
||||
# complete event is on its own line, so read those instead of the wrapper.
|
||||
for line in netlog.read_text().splitlines():
|
||||
try:
|
||||
event = json.loads(line.rstrip(','))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(event, dict) and 'type' in event:
|
||||
yield event
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def running(command, directory, env, label, startup_marker):
|
||||
log = directory / f'{label}.log'
|
||||
with log.open('w') as output:
|
||||
proc = subprocess.Popen(command, cwd=directory, env=env,
|
||||
stdout=output, stderr=subprocess.STDOUT)
|
||||
try:
|
||||
deadline = time.monotonic() + 10
|
||||
while True:
|
||||
if proc.poll() is not None:
|
||||
raise RuntimeError(f'{label} exited with {proc.returncode}')
|
||||
if startup_marker in log.read_text(errors='replace'):
|
||||
break
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError(f'{label} did not start within 10 seconds')
|
||||
time.sleep(0.05)
|
||||
yield proc
|
||||
finally:
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
|
||||
|
||||
class OriginHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Length', str(len(PAYLOAD)))
|
||||
self.end_headers()
|
||||
self.wfile.write(PAYLOAD)
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
def test_protocol(protocol, naive, caddy, directory, certs, origin_port):
|
||||
directory.mkdir()
|
||||
root_cert = certs / '2048-sha256-root.pem'
|
||||
empty_ca_file = directory / 'empty-ca.pem'
|
||||
empty_ca_file.write_text('')
|
||||
empty_ca_dir = directory / 'empty-ca-dir'
|
||||
empty_ca_dir.mkdir()
|
||||
trusted_ca_dir = directory / 'trusted-ca-dir'
|
||||
trusted_ca_dir.mkdir()
|
||||
# Only the CA belongs in this directory, not the leaf or any private keys.
|
||||
shutil.copyfile(root_cert, trusted_ca_dir / 'local-ca.pem')
|
||||
cases = (
|
||||
('untrusted', empty_ca_file, empty_ca_dir, False),
|
||||
('SSL_CERT_FILE', root_cert, empty_ca_dir, True),
|
||||
('SSL_CERT_DIR', empty_ca_file, trusted_ca_dir, True),
|
||||
('SSL_CERT_DIR-list', empty_ca_file,
|
||||
f'{empty_ca_dir}{os.pathsep}{trusted_ca_dir}', True),
|
||||
)
|
||||
caddy_port = unused_port(udp=protocol == 'quic')
|
||||
config = directory / 'Caddyfile'
|
||||
# Restrict the server to the requested transport so QUIC cannot pass by
|
||||
# falling back to HTTPS. No admin listener, port 80 redirect, ACME, or
|
||||
# system trust installation is needed. Only the local origin is allowed.
|
||||
protocols = 'h1 h2' if protocol == 'https' else 'h3'
|
||||
config.write_text(f'''{{
|
||||
admin off
|
||||
auto_https off
|
||||
persist_config off
|
||||
servers {{
|
||||
protocols {protocols}
|
||||
}}
|
||||
}}
|
||||
https://127.0.0.1:{caddy_port} {{
|
||||
bind 127.0.0.1
|
||||
tls "{certs / 'leaf_cert.pem'}" "{certs / 'leaf_cert.key'}"
|
||||
route {{
|
||||
forward_proxy {{
|
||||
basic_auth user pass
|
||||
ports {origin_port}
|
||||
acl {{
|
||||
allow 127.0.0.1
|
||||
deny all
|
||||
}}
|
||||
}}
|
||||
respond "Caddy local CA test"
|
||||
}}
|
||||
}}
|
||||
''')
|
||||
caddy_env = os.environ.copy()
|
||||
caddy_env['XDG_DATA_HOME'] = str(directory / 'data')
|
||||
caddy_env['XDG_CONFIG_HOME'] = str(directory / 'config')
|
||||
with running([caddy, 'run', '--config', str(config), '--adapter', 'caddyfile'],
|
||||
directory, caddy_env, 'caddy', 'serving initial configuration'):
|
||||
for label, ca_file, ca_dirs, trusted in cases:
|
||||
listen_port = unused_port()
|
||||
netlog = directory / f'{label}-netlog.json'
|
||||
env = os.environ.copy()
|
||||
# Override both variables, including system-default fallbacks, so
|
||||
# success can only come from the trust source this case supplies.
|
||||
env['SSL_CERT_FILE'] = str(ca_file)
|
||||
env['SSL_CERT_DIR'] = str(ca_dirs)
|
||||
env['TEST_MARK_STARTUP'] = 'yes'
|
||||
command = [naive, f'--listen=socks://127.0.0.1:{listen_port}',
|
||||
f'--proxy={protocol}://user:pass@127.0.0.1:{caddy_port}',
|
||||
'--log', f'--log-net-log={netlog}']
|
||||
with running(command, directory, env, label, 'TEST_MARK_STARTUP') as proc:
|
||||
# --disable ignores ~/.curlrc; --noproxy overrides NO_PROXY.
|
||||
# The origin uses HTTP: TLS verification under test belongs
|
||||
# to naive's connection to Caddy, not to curl.
|
||||
result = subprocess.run(
|
||||
['curl', '--disable', '--silent', '--show-error', '--fail',
|
||||
'--max-time', '10', '--noproxy', '', '--proxy',
|
||||
f'socks5h://127.0.0.1:{listen_port}',
|
||||
f'http://127.0.0.1:{origin_port}/'],
|
||||
capture_output=True, timeout=15)
|
||||
if proc.poll() is not None:
|
||||
raise RuntimeError(f'{protocol}/{label}: naive exited')
|
||||
if trusted:
|
||||
if result.returncode or result.stdout != PAYLOAD:
|
||||
raise RuntimeError(
|
||||
f'{protocol}/{label}: failed with trusted CA '
|
||||
f'(curl exit {result.returncode}):\n'
|
||||
f'{result.stderr.decode(errors="replace")}')
|
||||
else:
|
||||
if result.returncode == 0:
|
||||
raise RuntimeError(f'{protocol}: accepted an untrusted CA')
|
||||
# A timeout or connection failure alone cannot prove that
|
||||
# certificate verification ran. Require its authority error.
|
||||
if not any(event.get('params', {}).get('net_error') == -202
|
||||
for event in netlog_events(netlog)):
|
||||
raise RuntimeError(
|
||||
f'{protocol}: expected ERR_CERT_AUTHORITY_INVALID '
|
||||
f'with empty CA sources, curl exit {result.returncode}')
|
||||
print(f'PASS: {protocol} {label}', flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--naive', required=True, type=Path)
|
||||
parser.add_argument('--caddy', default=TESTS_DIR / 'caddy', type=Path)
|
||||
args = parser.parse_args()
|
||||
naive, caddy = str(args.naive.resolve()), str(args.caddy.resolve())
|
||||
for binary in (naive, caddy):
|
||||
if not os.access(binary, os.X_OK):
|
||||
parser.error(f'Not an executable: {binary}')
|
||||
for tool in ('openssl', 'curl'):
|
||||
if shutil.which(tool) is None:
|
||||
parser.error(f'Required executable not found: {tool}')
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='naive-ssl-cert-file-') as tmp:
|
||||
directory = Path(tmp)
|
||||
try:
|
||||
certs = generate_certs(directory / 'certs')
|
||||
with http.server.ThreadingHTTPServer(
|
||||
('127.0.0.1', 0), OriginHandler) as origin:
|
||||
thread = threading.Thread(target=origin.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
for protocol in ('https', 'quic'):
|
||||
test_protocol(protocol, naive, caddy, directory / protocol,
|
||||
certs, origin.server_port)
|
||||
finally:
|
||||
origin.shutdown()
|
||||
thread.join()
|
||||
except Exception as error:
|
||||
print(f'FAIL: {error}', file=sys.stderr)
|
||||
for log in sorted(directory.glob('*/*.log')):
|
||||
print(f'\n--- {log.parent.name}/{log.name} ---', file=sys.stderr)
|
||||
print(log.read_text(errors='replace'), file=sys.stderr)
|
||||
for netlog in sorted(directory.glob('*/*-netlog.json')):
|
||||
print(f'\n--- {netlog.parent.name}/{netlog.name} verification/errors ---',
|
||||
file=sys.stderr)
|
||||
for event in netlog_events(netlog):
|
||||
params = event.get('params', {})
|
||||
if ('cert_status' in params or
|
||||
any('error' in key and value for key, value in params.items())):
|
||||
print({key: value for key, value in params.items()
|
||||
if 'error' in key or key in (
|
||||
'details', 'cert_status', 'is_issued_by_known_root')},
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user