Files
2026-09-21 17:30:02 +08:00

129 lines
5.5 KiB
Python

"""Verify the cloud IP HTTPS deployment without printing credentials or records.
Run on the server: python3 scripts/verify_cloud_https.py
Uses the system CA store and verifies the server certificate, including its IP.
Only login/logout and read-only aggregate queries are submitted.
"""
import hashlib
import http.cookiejar
import json
from pathlib import Path
import ssl
import sys
from urllib import error, request
ROOT = Path(__file__).resolve().parents[1]
BASE = "https://203.195.219.128"
API = "/wolin/api/studentsManagement/workspace"
MODULES = ("students", "classes", "teachers", "advisors", "scores", "employment")
class CheckFailed(Exception):
pass
def require(condition, message):
if not condition:
raise CheckFailed(message)
class NoRedirect(request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
def main():
cookies = http.cookiejar.CookieJar()
client = request.build_opener(
request.ProxyHandler({}), NoRedirect(),
request.HTTPSHandler(context=ssl.create_default_context()),
request.HTTPCookieProcessor(cookies),
)
def check(path, status=200, data=None, headers=None):
headers = dict(headers or {})
if data is not None:
headers.update({"Content-Type": "application/json", "Origin": BASE})
req = request.Request(BASE + path, headers=headers,
data=None if data is None else json.dumps(data).encode())
try:
response = client.open(req, timeout=30)
except error.HTTPError as exc:
response = exc
except error.URLError as exc:
message = ("TLS certificate verification failed" if isinstance(
exc.reason, ssl.SSLCertVerificationError) else "HTTPS connection failed")
raise CheckFailed(message) from None
with response:
body = response.read()
require(response.status == status,
f"{path}: expected HTTP {status}, received {response.status}")
require("WWW-Authenticate" not in response.headers,
f"{path}: unexpected native authentication challenge")
return body, response.headers
portal, _ = check("/")
require(b'href="/blackjack/"' in portal and b'href="/wolin/"' in portal,
"Portal entry links missing")
game, _ = check("/blackjack/")
require(hashlib.sha256(game).digest() == hashlib.sha256(
(ROOT / "deploy/site/blackjack/index.html").read_bytes()).digest(),
"Served game differs from preserved build")
_, headers = check("/wolin/", 303, headers={"Accept": "text/html"})
require(headers.get("Location") == "/wolin/login", "Login redirect incorrect")
login, _ = check("/wolin/login")
require(b'id="login-form"' in login, "Login form missing")
for path in (API + "/overview", "/wolin/docs", "/wolin/openapi.json"):
check(path, 401)
check(API + "/query", 401, {"module": "students", "aggregate": "count"})
# Read credentials only after successful certificate and public-route checks.
try:
credentials = dict(line.split(": ", 1) for line in
(ROOT / "artifacts/cloud-access.txt").read_text(
encoding="utf-8").splitlines() if ": " in line)
payload = {"username": credentials["Username"], "password": credentials["Password"]}
except (OSError, ValueError, KeyError):
raise CheckFailed("Administrator credentials file missing or invalid") from None
body, _ = check("/wolin/auth/login", data=payload)
require(json.loads(body).get("success") is True, "Login did not succeed")
try:
session = next((cookie for cookie in cookies if cookie.name == "wolin_session"), None)
require(session is not None, "Session cookie missing")
require(session.secure and session.has_nonstandard_attr("HttpOnly")
and session.path == "/wolin/"
and str(session.get_nonstandard_attr("SameSite")).lower() == "strict",
"Session cookie must have Secure, HttpOnly, SameSite=Strict and /wolin/ path")
_, headers = check("/wolin/")
require(headers.get("Cache-Control") == "no-store", "Private page cache policy incorrect")
body, _ = check(API + "/overview")
overview = json.loads(body)["counts"]
counts = {}
for module in MODULES:
body, _ = check(API + "/query", data={"module": module, "aggregate": "count"})
count = json.loads(body)["items"][0]["value"]
require(type(count) is int and count >= 0 and count == overview[module],
f"{module}: aggregate and overview counts differ")
counts[module] = count
finally:
check("/wolin/auth/logout", data={})
require(not list(cookies), "Logout did not clear session cookie")
check(API + "/overview", 401)
return {"status": "ok", "tls_verification": "ok", "portal": "ok",
"preserved_game": "ok", "secure_login_cookie": "ok",
"unauthenticated_access": "401", "six_modules": "ok",
"logout_access": "401", "counts": counts}
if __name__ == "__main__":
try:
report = main()
except CheckFailed as exc:
print(json.dumps({"status": "failed", "check": str(exc)}))
sys.exit(1)
except Exception as exc:
# Suppress response bodies, credential contents and tracebacks.
print(json.dumps({"status": "failed", "error_type": type(exc).__name__}))
sys.exit(1)
print(json.dumps(report))