ctf-ad-mirror-tool-rust/scripts/verify-linux-package.py

117 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Test the shipped archive in isolation; never print generated credentials."""
import http.cookiejar
import json
import os
from pathlib import Path
import socket
import stat
import subprocess
import sys
import tarfile
import tempfile
import threading
import time
import urllib.request
def free_port():
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
return sock.getsockname()[1]
with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary:
root = Path(temporary)
with tarfile.open(sys.argv[1]) as archive:
archive.extractall(root, filter="data")
package = root / "mirror-tool-linux-x64"
env = os.environ.copy()
env.pop("MIRROR_PASSWORD", None)
subprocess.run([str(package / "run.sh"), "check"], cwd=root, env=env, check=True)
password_path = package / "data/panel-password"
password = password_path.read_text()
assert len(password) == 48
assert stat.S_IMODE(password_path.stat().st_mode) == 0o600
config = package / "mirror.toml"
web_port = free_port()
config.write_text(config.read_text().replace("127.0.0.1:6700", f"127.0.0.1:{web_port}"))
original_config = config.read_bytes()
base = f"http://127.0.0.1:{web_port}"
upstream = socket.socket()
upstream.bind(("127.0.0.1", 0))
upstream.listen()
upstream.settimeout(15)
received = bytearray()
def echo():
connection, _ = upstream.accept()
with connection:
connection.settimeout(10)
while data := connection.recv(65536):
received.extend(data)
connection.sendall(data)
thread = threading.Thread(target=echo, daemon=True)
thread.start()
proxy_port = free_port()
for restart in (False, True):
with (root / "server.log").open("wb") as log:
process = subprocess.Popen([str(package / "run.sh")], cwd=root, env=env, stdout=log, stderr=log)
try:
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
for _ in range(100):
if process.poll() is not None:
raise RuntimeError("packaged process exited during startup")
try:
with opener.open(base + "/", timeout=1) as response:
assert b"html" in response.read().lower()
break
except OSError:
time.sleep(0.1)
else:
raise RuntimeError("packaged server did not become ready")
def request(path, value=None, method=None, csrf=None):
headers = {"Content-Type": "application/json"}
if csrf:
headers["x-csrf-token"] = csrf
req = urllib.request.Request(base + path, data=None if value is None else json.dumps(value).encode(), headers=headers, method=method)
with opener.open(req, timeout=3) as response:
return json.load(response)
csrf = request("/api/login", {"password": password})["csrf"]
if not restart:
snapshot = request("/api/snapshot")
assert snapshot["teams"] == [] and snapshot["services"] == []
assert snapshot["flag_protection"] is None
request("/api/teams/local", {"id": "local", "name": "Local", "address": "127.0.0.1"}, "PUT", csrf)
service = {"id": "echo", "name": "Echo", "listen": f"127.0.0.1:{proxy_port}", "upstream_port": upstream.getsockname()[1], "primary": "local", "replicas": [], "enabled": True, "auto_switch": False}
request("/api/services/echo", service, "PUT", csrf)
payload = bytes(range(256)) * 1024
with socket.create_connection(("127.0.0.1", proxy_port), timeout=5) as connection:
connection.sendall(payload)
connection.shutdown(socket.SHUT_WR)
reply = bytearray()
while part := connection.recv(65536):
reply.extend(part)
thread.join(5)
assert not thread.is_alive()
assert reply == received == payload
snapshot = request("/api/snapshot")
assert snapshot["services"][0]["flag_protection"] is False
assert snapshot["services"][0]["enabled"] is True
assert password_path.read_text() == password
assert config.read_bytes() == original_config
finally:
process.terminate()
try:
process.wait(timeout=20)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
raise
assert process.returncode == 0
upstream.close()
print("Package OK: first start, private password, empty topology, login, API persistence/restart, exact 256KiB TCP half-close, flag substitution off.")