mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-20 16:00:34 +02:00
146 lines
7.2 KiB
Python
146 lines
7.2 KiB
Python
"""Exercise real CA composition, native path mapping, and HTTPS CONNECT routing."""
|
|
import os
|
|
from pathlib import Path
|
|
import socket
|
|
import ssl
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
|
|
|
|
def command(*args):
|
|
subprocess.run(args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
|
|
|
|
def main():
|
|
probe, openssl = sys.argv[1:]
|
|
|
|
with tempfile.TemporaryDirectory(prefix="mocktail-fleasion-test-") as directory:
|
|
root = Path(directory)
|
|
for name in ("public", "fleasion", "rotated"):
|
|
command(openssl, "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "2",
|
|
"-subj", f"/CN={name}", "-addext", "basicConstraints=critical,CA:TRUE",
|
|
"-keyout", str(root / f"{name}.key"), "-out", str(root / f"{name}.crt"))
|
|
command(openssl, "req", "-newkey", "rsa:2048", "-nodes", "-subj",
|
|
"/CN=assetdelivery.roblox.com", "-keyout", str(root / "leaf.key"),
|
|
"-out", str(root / "leaf.csr"))
|
|
(root / "leaf.ext").write_text("subjectAltName=DNS:assetdelivery.roblox.com\nbasicConstraints=CA:FALSE\n")
|
|
command(openssl, "x509", "-req", "-in", str(root / "leaf.csr"), "-CA",
|
|
str(root / "fleasion.crt"), "-CAkey", str(root / "fleasion.key"),
|
|
"-CAcreateserial", "-days", "1", "-extfile", str(root / "leaf.ext"),
|
|
"-out", str(root / "leaf.crt"))
|
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
context.load_cert_chain(root / "leaf.crt", root / "leaf.key")
|
|
intercepted = []
|
|
failures = []
|
|
listener = socket.socket()
|
|
listener.bind(("127.0.0.1", 0))
|
|
listener.listen()
|
|
listener.settimeout(5)
|
|
port = listener.getsockname()[1]
|
|
|
|
def serve():
|
|
try:
|
|
# Correct CA succeeds; the unrelated CA must fail TLS verification.
|
|
for attempt in range(2):
|
|
connection, _ = listener.accept()
|
|
with connection:
|
|
connection.settimeout(5)
|
|
connect = b""
|
|
while not connect.endswith(b"\r\n\r\n"):
|
|
data = connection.recv(4096)
|
|
if not data:
|
|
raise AssertionError("proxy connection closed before CONNECT")
|
|
connect += data
|
|
assert connect.startswith(b"CONNECT assetdelivery.roblox.com:443 "), connect
|
|
connection.sendall(b"HTTP/1.1 200 Connection established\r\n\r\n")
|
|
try:
|
|
with context.wrap_socket(connection, server_side=True) as tls:
|
|
request = tls.recv(8192)
|
|
assert request.startswith(b"GET /replacement "), request
|
|
intercepted.append(request)
|
|
body = b"fleasion-replaced-asset"
|
|
tls.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: " +
|
|
str(len(body)).encode() + b"\r\nConnection: close\r\n\r\n" + body)
|
|
except (ssl.SSLError, ConnectionError):
|
|
if attempt == 0:
|
|
raise
|
|
# The wrong CA may close with either a TLS alert or
|
|
# a TCP reset before the server finishes its handshake.
|
|
except BaseException as error:
|
|
failures.append(error)
|
|
|
|
thread = threading.Thread(target=serve, daemon=True)
|
|
thread.start()
|
|
env = {key: value for key, value in os.environ.items()
|
|
if not key.startswith(("MOCKTAIL_", "ROBLOX_"))}
|
|
for key in ("http_proxy", "https_proxy", "all_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"):
|
|
env.pop(key, None)
|
|
env.update(HOME=str(root), XDG_CONFIG_HOME=str(root / "config"),
|
|
MOCKTAIL_CACHE_ROOT=str(root / "cache"), NO_PROXY="*", no_proxy="*")
|
|
certificate = root / "config/Fleasion/proxy_ca/ca.crt"
|
|
certificate.parent.mkdir(parents=True)
|
|
original = (root / "fleasion.crt").read_bytes()
|
|
certificate.write_bytes(original)
|
|
config = root / "config.yaml"
|
|
|
|
def configure(extra="", enabled="true", mode="env"):
|
|
config.write_text(f"version: 1\nnetwork:\n ca_bundle: {root / 'public.crt'}\n"
|
|
f"integrations:\n fleasion:\n enabled: {enabled}\n"
|
|
f" proxy_mode: {mode}\n proxy_port: {port}\n{extra}")
|
|
|
|
def run(url="prepare", success=True, environment=None):
|
|
result = subprocess.run([probe, str(config), url], env=environment or env,
|
|
capture_output=True, text=True, timeout=10)
|
|
assert (result.returncode == 0) == success, result.stdout + result.stderr
|
|
return result
|
|
|
|
configure()
|
|
response = run("https://assetdelivery.roblox.com/replacement")
|
|
assert "fleasion-replaced-asset" in response.stdout
|
|
assert certificate.read_bytes() == original
|
|
generated = root / "cache/fleasion/cacert.pem"
|
|
assert generated.stat().st_mode & 0o777 == 0o600
|
|
command(openssl, "verify", "-CAfile", str(generated), str(root / "public.crt"))
|
|
first_bundle = generated.read_bytes()
|
|
run() # Repeated startup must not duplicate trust roots.
|
|
assert generated.read_bytes() == first_bundle
|
|
certificate.write_bytes((root / "rotated.crt").read_bytes())
|
|
response = run("https://assetdelivery.roblox.com/replacement", success=False)
|
|
assert response.returncode == 10, response.stderr
|
|
thread.join(timeout=7)
|
|
listener.close()
|
|
assert not thread.is_alive() and not failures, failures
|
|
assert len(intercepted) == 1
|
|
assert original not in generated.read_bytes()
|
|
command(openssl, "verify", "-CAfile", str(generated), str(root / "rotated.crt"))
|
|
# Re-exec must compose from the original public roots, not old proxy CAs.
|
|
inherited = dict(env, MOCKTAIL_CA_BUNDLE=str(generated),
|
|
MOCKTAIL_FLEASION_GENERATED_BUNDLE=str(generated),
|
|
MOCKTAIL_FLEASION_BASE_CA_BUNDLE=str(root / "public.crt"))
|
|
certificate.write_bytes(original)
|
|
run(environment=inherited)
|
|
assert generated.read_bytes() == first_bundle
|
|
for bad in (b"not a certificate", (root / "leaf.crt").read_bytes()):
|
|
certificate.write_bytes(bad)
|
|
run(success=False)
|
|
assert generated.read_bytes() == first_bundle
|
|
certificate.unlink()
|
|
run(success=False)
|
|
configure(enabled="false")
|
|
run() # Disabled integration does not require Fleasion.
|
|
certificate.write_bytes(original)
|
|
configure(mode="hosts")
|
|
run()
|
|
configure(mode="invalid")
|
|
run(success=False)
|
|
configure(extra=" ca_certificate: relative.crt\n")
|
|
run(success=False)
|
|
print("Fleasion integration: proxy replacement, TLS rejection, CA rotation, "
|
|
"native aliases, and configuration checks passed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|