feat: multi-team orchestrator - topology UI, team management, flag randomizer, public submit + leaderboard
- panel/teams.py: create/start/stop teams with isolated ports+creds, per-team receivers with CHALLENGE_PORT/CONTAINER env, flag randomization, submit validation + leaderboard
- panel/main.py: /api/teams, /api/teams/{idx}/randomize, /api/flag/submit, /api/leaderboard, /api/public/teams, /submit page
- panel/static/submit.html: public flag submission UI for teams
- receiver: _ch_port/_ch_container read .env per team, container name overrides
- .gitignore: exclude teams/, .venv, __pycache__
This commit is contained in:
+170
-1
@@ -14,6 +14,8 @@ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from typing import Optional
|
||||
|
||||
import teams as orch
|
||||
|
||||
RECEIVER_URL = os.environ.get("RECEIVER_URL", "http://127.0.0.1:18080")
|
||||
ADMIN_USER = os.environ.get("PANEL_ADMIN_USER", "admin")
|
||||
ADMIN_PASS = os.environ.get("PANEL_ADMIN_PASS", "admin")
|
||||
@@ -88,6 +90,11 @@ async def login_page(req: Request):
|
||||
return RedirectResponse("/")
|
||||
return HTMLResponse((BASE_DIR / "static" / "login.html").read_text())
|
||||
|
||||
@app.get("/submit", response_class=HTMLResponse)
|
||||
async def submit_page(req: Request):
|
||||
"""Public flag submission page for teams (no login)."""
|
||||
return HTMLResponse((BASE_DIR / "static" / "submit.html").read_text())
|
||||
|
||||
@app.post("/api/login")
|
||||
async def api_login(req: Request):
|
||||
data = await req.json()
|
||||
@@ -190,4 +197,166 @@ async def api_history(req: Request):
|
||||
lines = (BASE_DIR.parent / "history" / "command.txt").read_text().splitlines()
|
||||
except Exception:
|
||||
lines = []
|
||||
return {"lines": lines[-200:]}
|
||||
return {"lines": lines[-200:]}
|
||||
|
||||
# ============ Multi-team orchestrator endpoints ============
|
||||
|
||||
@app.get("/api/teams")
|
||||
async def api_teams(req: Request):
|
||||
require_login(req)
|
||||
return {"teams": orch.list_teams()}
|
||||
|
||||
@app.post("/api/teams/set")
|
||||
async def api_teams_set(req: Request):
|
||||
"""Set/create N teams (idempotent: creates missing, keeps existing)."""
|
||||
require_login(req)
|
||||
data = await req.json()
|
||||
n = int(data.get("count", 0))
|
||||
if n < 0 or n > 50:
|
||||
raise HTTPException(400, "Team count must be 0-50")
|
||||
created = []
|
||||
for i in range(1, n + 1):
|
||||
td = orch.TEAMS_DIR / f"team{i}"
|
||||
if not td.exists():
|
||||
st = orch.create_team(i, data.get("label_prefix", "Tim"))
|
||||
created.append(st["index"])
|
||||
return {"created": created, "total": len(orch.list_teams())}
|
||||
|
||||
@app.post("/api/teams/start")
|
||||
async def api_teams_start(req: Request):
|
||||
require_login(req)
|
||||
data = await req.json()
|
||||
idx = data.get("index")
|
||||
if idx is None:
|
||||
# start all
|
||||
results = []
|
||||
for t in orch.list_teams():
|
||||
try:
|
||||
results.append({"team": t["index"], "ok": True})
|
||||
orch.start_team(t["index"])
|
||||
except Exception as e:
|
||||
results.append({"team": t["index"], "ok": False, "err": str(e)})
|
||||
return {"results": results}
|
||||
try:
|
||||
st = orch.start_team(int(idx))
|
||||
return {"ok": True, "team": st}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
@app.post("/api/teams/stop")
|
||||
async def api_teams_stop(req: Request):
|
||||
require_login(req)
|
||||
data = await req.json()
|
||||
idx = data.get("index")
|
||||
if idx is None:
|
||||
results = []
|
||||
for t in orch.list_teams():
|
||||
try:
|
||||
orch.stop_team(t["index"])
|
||||
results.append({"team": t["index"], "ok": True})
|
||||
except Exception as e:
|
||||
results.append({"team": t["index"], "ok": False, "err": str(e)})
|
||||
return {"results": results}
|
||||
try:
|
||||
st = orch.stop_team(int(idx))
|
||||
return {"ok": True, "team": st}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
@app.get("/api/teams/{idx}/logs")
|
||||
async def api_team_logs(idx: int, req: Request, service: Optional[str] = None, tail: int = 100):
|
||||
require_login(req)
|
||||
try:
|
||||
logs = orch.team_logs(idx, service, tail)
|
||||
return {"team": idx, "logs": logs}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
@app.get("/api/teams/{idx}/creds")
|
||||
async def api_team_creds(idx: int, req: Request):
|
||||
require_login(req)
|
||||
try:
|
||||
td = orch.TEAMS_DIR / f"team{idx}"
|
||||
st = json.loads((td / "state.json").read_text())
|
||||
return {"team": st}
|
||||
except Exception as e:
|
||||
raise HTTPException(404, str(e))
|
||||
|
||||
@app.get("/api/topology")
|
||||
async def api_topology(req: Request):
|
||||
"""Return topology graph data (nodes + edges) for the UI."""
|
||||
require_login(req)
|
||||
teams = orch.list_teams()
|
||||
nodes = [
|
||||
{"id": "panel", "label": "Panel A/D", "type": "panel", "url": "https://panel.gemastik.imrnes.team"},
|
||||
{"id": "traefik", "label": "Traefik / Coolify", "type": "infra"},
|
||||
{"id": "dns", "label": "*.imrnes.team → 43.134.105.109", "type": "infra"},
|
||||
]
|
||||
edges = [{"from": "dns", "to": "traefik"}, {"from": "traefik", "to": "panel"}]
|
||||
for t in teams:
|
||||
nid = f"team{t['index']}"
|
||||
nodes.append({
|
||||
"id": nid, "label": t.get("label", f"Team {t['index']}"),
|
||||
"type": "team", "index": t["index"], "status": t.get("status", "unknown"),
|
||||
"receiver_port": t["ports"]["receiver"], "ssh_pass": t.get("ssh_pass", ""),
|
||||
})
|
||||
edges.append({"from": "traefik", "to": nid, "label": f":{t['ports']['receiver']}"})
|
||||
for name, coff, soff in orch.CHALLENGES:
|
||||
cn = f"team{t['index']}-{name}"
|
||||
nodes.append({"id": cn, "label": f"{name}", "type": "challenge",
|
||||
"port": t["ports"][name]["chall"], "ssh": t["ports"][name]["ssh"]})
|
||||
edges.append({"from": nid, "to": cn, "label": f":{t['ports'][name]['chall']}"})
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
|
||||
|
||||
# ============ Flag randomization + team submission ============
|
||||
|
||||
@app.post("/api/teams/{idx}/randomize")
|
||||
async def api_randomize(idx: int, req: Request):
|
||||
"""Randomize all flags for a team (recreates containers to pick up new flags)."""
|
||||
require_login(req)
|
||||
try:
|
||||
mapping = orch.randomize_flags(idx)
|
||||
return {"ok": True, "team": idx, "flags": mapping}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/api/flag/submit")
|
||||
async def api_flag_submit(req: Request):
|
||||
"""Public endpoint: teams submit flags. No login needed."""
|
||||
data = await req.json()
|
||||
team = int(data.get("team", 0))
|
||||
chall = data.get("challenge", "")
|
||||
flag = data.get("flag", "").strip()
|
||||
team_name = data.get("team_name", "").strip()[:64] or f"Team {team}"
|
||||
if team <= 0:
|
||||
return {"success": False, "error": "Select your team"}
|
||||
if chall not in [c[0] for c in orch.CHALLENGES]:
|
||||
return {"success": False, "error": "Challenge not found"}
|
||||
if not flag:
|
||||
return {"success": False, "error": "Flag is required"}
|
||||
return orch.submit_flag(team, chall, flag, team_name)
|
||||
|
||||
|
||||
@app.get("/api/leaderboard")
|
||||
async def api_leaderboard(req: Request):
|
||||
"""Leaderboard of solves so far."""
|
||||
lb_path = orch.TEAMS_DIR / "leaderboard.json"
|
||||
if lb_path.exists():
|
||||
lb = json.loads(lb_path.read_text())
|
||||
else:
|
||||
lb = {"solves": []}
|
||||
# aggregate per team
|
||||
teams = {}
|
||||
for e in lb["solves"]:
|
||||
t = teams.setdefault(e["team"], {"team": e["team"], "name": e["team_name"], "solves": 0, "challs": []})
|
||||
t["solves"] += 1
|
||||
t["challs"].append(e["challenge"])
|
||||
return {"solves": lb["solves"], "teams": sorted(teams.values(), key=lambda x: -x["solves"])}
|
||||
|
||||
|
||||
@app.get("/api/public/teams")
|
||||
async def api_public_teams():
|
||||
"""Public list of team names (for the submit dropdown)."""
|
||||
return {"teams": [{"index": t["index"], "label": t.get("label", f"Team {t['index']}")} for t in orch.list_teams()]}
|
||||
Reference in New Issue
Block a user