2026-03-20 13:40:13 +08:00
|
|
|
|
import re
|
|
|
|
|
|
from dataclasses import dataclass
|
2026-03-24 01:22:50 +08:00
|
|
|
|
from pathlib import Path
|
2026-03-20 13:40:13 +08:00
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-24 01:22:50 +08:00
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
|
|
ROUTER_GO = PROJECT_ROOT / "soul-api" / "internal" / "router" / "router.go"
|
2026-03-20 13:40:13 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class Check:
|
|
|
|
|
|
method: str
|
|
|
|
|
|
path: str
|
|
|
|
|
|
status: int
|
|
|
|
|
|
preview: str
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-24 01:22:50 +08:00
|
|
|
|
def _read_text(path: Path) -> str:
|
2026-03-20 13:40:13 +08:00
|
|
|
|
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
|
|
|
|
return f.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_routes() -> list[tuple[str, str]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
返回 [(method, full_path_template), ...]
|
|
|
|
|
|
full_path_template 保留 :id 占位符。
|
|
|
|
|
|
"""
|
|
|
|
|
|
text = _read_text(ROUTER_GO)
|
|
|
|
|
|
routes: list[tuple[str, str]] = []
|
|
|
|
|
|
|
|
|
|
|
|
# /api/admin 登录/鉴权/登出
|
|
|
|
|
|
for m in re.finditer(r'api\.(GET|POST|PUT|DELETE)\("(/admin(?:/[^"]*)?)",\s*handler\.[A-Za-z0-9_]+', text):
|
|
|
|
|
|
routes.append((m.group(1), f"/api{m.group(2)}"))
|
|
|
|
|
|
|
|
|
|
|
|
# /api/admin 组
|
|
|
|
|
|
for m in re.finditer(r'admin\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
|
|
|
|
|
routes.append((m.group(1), f"/api/admin{m.group(2)}"))
|
|
|
|
|
|
|
|
|
|
|
|
# /api/db 组
|
|
|
|
|
|
for m in re.finditer(r'db\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
|
|
|
|
|
routes.append((m.group(1), f"/api/db{m.group(2)}"))
|
|
|
|
|
|
|
|
|
|
|
|
# 去重
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
out = []
|
|
|
|
|
|
for method, p in routes:
|
|
|
|
|
|
if (method, p) in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add((method, p))
|
|
|
|
|
|
out.append((method, p))
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def replace_path_params(path: str) -> str:
|
|
|
|
|
|
return path.replace(":id", "1")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
|
|
api_base = (os.environ.get("SOUL_API_BASE") or "http://localhost:8080").rstrip("/")
|
|
|
|
|
|
session = requests.Session()
|
|
|
|
|
|
session.verify = False # 如为 https 自签证书也可探测
|
|
|
|
|
|
|
|
|
|
|
|
routes = extract_routes()
|
|
|
|
|
|
print(f"Found routes: {len(routes)}")
|
|
|
|
|
|
|
|
|
|
|
|
failures: list[Check] = []
|
|
|
|
|
|
unexpected: list[Check] = []
|
|
|
|
|
|
|
|
|
|
|
|
headers = {"Content-Type": "application/json"}
|
|
|
|
|
|
|
|
|
|
|
|
# 先验证登录接口是否通(只对 /api/admin POST 登录做一次带凭证的检查)
|
|
|
|
|
|
admin_username = os.environ.get("SOUL_ADMIN_USERNAME", "admin")
|
|
|
|
|
|
admin_password = os.environ.get("SOUL_ADMIN_PASSWORD", "admin123")
|
|
|
|
|
|
login_url = f"{api_base}/api/admin"
|
|
|
|
|
|
r_login = session.post(
|
|
|
|
|
|
login_url,
|
|
|
|
|
|
json={"username": admin_username, "password": admin_password},
|
|
|
|
|
|
headers=headers,
|
|
|
|
|
|
timeout=10,
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
login_data = r_login.json()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
login_data = None
|
|
|
|
|
|
if r_login.status_code != 200 or not (login_data and login_data.get("success") is True and login_data.get("token")):
|
|
|
|
|
|
failures.append(Check("POST", "/api/admin", r_login.status_code, (r_login.text or "")[:200]))
|
|
|
|
|
|
print("LOGIN_CHECK_FAILED,后续路由鉴权探测可能不准确。")
|
|
|
|
|
|
|
|
|
|
|
|
for method, path_template in routes:
|
|
|
|
|
|
path = replace_path_params(path_template)
|
|
|
|
|
|
url = f"{api_base}{path}"
|
|
|
|
|
|
|
|
|
|
|
|
# 仅对登录接口放行;其他都不带 token,避免触发写操作
|
|
|
|
|
|
json_payload = None
|
|
|
|
|
|
if path == "/api/admin" and method == "POST":
|
|
|
|
|
|
# 已在上面验证登录;这里跳过
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if method in ("POST", "PUT"):
|
|
|
|
|
|
# 发空 body,通常也会被 AdminAuth 在更早阶段拦截
|
|
|
|
|
|
json_payload = {}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
resp = session.request(method, url, headers=headers, json=json_payload, timeout=10)
|
|
|
|
|
|
status = resp.status_code
|
|
|
|
|
|
preview = (resp.text or "")[:200].replace("\n", " ")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
failures.append(Check(method, path, 0, f"EXC: {e}"))
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 非登录接口:预期 AdminAuth 拦截 => 401 或 403
|
|
|
|
|
|
if status not in (401, 403):
|
|
|
|
|
|
unexpected.append(Check(method, path, status, preview))
|
|
|
|
|
|
|
|
|
|
|
|
print("\n=== AUTHLESS_SMOKE_RESULT ===")
|
|
|
|
|
|
print("Failures(0/404/500 等异常/网络异常):", len(failures))
|
|
|
|
|
|
for it in failures[:30]:
|
|
|
|
|
|
print(f"- {it.method} {it.path} -> {it.status}, preview={it.preview}")
|
|
|
|
|
|
|
|
|
|
|
|
print("Unexpected (非 401/403):", len(unexpected))
|
|
|
|
|
|
for it in unexpected[:30]:
|
|
|
|
|
|
print(f"- {it.method} {it.path} -> {it.status}, preview={it.preview}")
|
|
|
|
|
|
|
|
|
|
|
|
if len(unexpected) > 30:
|
|
|
|
|
|
print("... truncated")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|
|
|
|
|
|
|