112 lines
4.5 KiB
Python
Executable File
112 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""回归检查生成的 JSRPC/Flask/Burp 交付契约。"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import importlib.util
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any
|
||
from urllib.parse import urlencode
|
||
|
||
|
||
def load_module(path: Path):
|
||
spec = importlib.util.spec_from_file_location("jsra_delivery_contract_proxy", path)
|
||
if spec is None or spec.loader is None:
|
||
raise RuntimeError(f"无法加载代理:{path}")
|
||
module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(module)
|
||
return module
|
||
|
||
|
||
def check_endpoint_normalization(module) -> dict[str, Any]:
|
||
calls: list[str] = []
|
||
|
||
class FakeResponse:
|
||
def raise_for_status(self) -> None:
|
||
return None
|
||
|
||
def json(self) -> dict[str, Any]:
|
||
return {"data": {"requestBody": "loginId=admin&password2=ENC"}}
|
||
|
||
def fake_get(url: str, **_: Any) -> FakeResponse:
|
||
calls.append(url)
|
||
return FakeResponse()
|
||
|
||
original_get = module.requests.get
|
||
try:
|
||
module.requests.get = fake_get
|
||
transform = module.CONFIG["transforms"][0]
|
||
module.jsrpc_call(transform.get("action", module.CONFIG["jsrpc"]["action"]), "123456", transform)
|
||
finally:
|
||
module.requests.get = original_get
|
||
|
||
if len(calls) != 1 or not calls[0].endswith("/go") or calls[0].endswith("/go/go"):
|
||
raise AssertionError(f"JSRPC endpoint 归一化失败:{calls}")
|
||
return {"ok": True, "called": calls[0]}
|
||
|
||
|
||
def check_packet_modes(module) -> dict[str, Any]:
|
||
transform = module.CONFIG["transforms"][0]
|
||
path = str(transform.get("path") or transform.get("id") or "password")
|
||
field = path.removeprefix("$.").split(".")[-1]
|
||
if transform.get("delivery_mode") == "request_body":
|
||
fake_result = f"loginId=admin&{field}=ENC(123456)"
|
||
expected_body = fake_result
|
||
else:
|
||
fake_result = "ENC(123456)"
|
||
expected_body = f"loginId=admin&{urlencode({field: 'ENC(123456)'})}"
|
||
module.jsrpc_call = lambda _action, _value, _transform: fake_result
|
||
client = module.app.test_client()
|
||
raw_packet = (
|
||
"POST /newlogin/login.do?appName=arena&fromSite=77 HTTP/1.1\r\n"
|
||
"Host: 127.0.0.1:8123\r\n"
|
||
"Content-Type: application/x-www-form-urlencoded\r\n"
|
||
"Connection: close\r\n\r\n"
|
||
f"loginId=admin&{field}=123456"
|
||
)
|
||
packet_response = client.post("/encode", data=raw_packet, content_type="application/octet-stream")
|
||
packet_text = packet_response.get_data(as_text=True)
|
||
if packet_response.status_code != 200:
|
||
raise AssertionError(f"整包模式失败:HTTP {packet_response.status_code}: {packet_text}")
|
||
if expected_body not in packet_text:
|
||
raise AssertionError(f"整包模式没有替换 body:{packet_text}")
|
||
expected_length = len(expected_body.encode())
|
||
if f"Content-Length: {expected_length}" not in packet_text:
|
||
raise AssertionError(f"整包模式没有同步 Content-Length:{packet_text}")
|
||
|
||
wrapper_response = client.post(
|
||
"/encode",
|
||
data={"dataBody": f"loginId=admin&{field}=123456", "requestorresponse": "request"},
|
||
content_type="application/x-www-form-urlencoded",
|
||
)
|
||
wrapper_text = wrapper_response.get_data(as_text=True)
|
||
expected_wrapper = expected_body
|
||
if wrapper_response.status_code != 200 or wrapper_text != expected_wrapper:
|
||
raise AssertionError(f"dataBody wrapper 失败:HTTP {wrapper_response.status_code}: {wrapper_text}")
|
||
return {"ok": True, "packet_status": packet_response.status_code, "wrapper_status": wrapper_response.status_code}
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="Validate generated Flask delivery contracts.")
|
||
parser.add_argument("--proxy", required=True, help="Generated flask_proxy.py path.")
|
||
parser.add_argument("--output", required=True, help="JSON report path.")
|
||
args = parser.parse_args()
|
||
module = load_module(Path(args.proxy))
|
||
result = {
|
||
"passed": True,
|
||
"proxy": str(Path(args.proxy).resolve()),
|
||
"endpoint_normalization": check_endpoint_normalization(module),
|
||
"packet_modes": check_packet_modes(module),
|
||
"live_action_required": True,
|
||
}
|
||
output = Path(args.output)
|
||
output.parent.mkdir(parents=True, exist_ok=True)
|
||
output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
print(json.dumps(result, ensure_ascii=False))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|