"""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))