项目代码提交
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user