ctfy.sdk.resources.awd
client.awd — classic-AWD attack submissions.
1"""``client.awd`` — classic-AWD attack submissions.""" 2 3from __future__ import annotations 4 5import builtins 6 7from ctfy.sdk._helpers import _raise_for_status 8from ctfy.sdk.base import BaseHttpClient 9from ctfy.server.models import AwdBoardResponse, AwdFlagSubmitResponse, AwdMatchStatus 10 11 12class AwdResource: 13 """A classic-AWD match: submit captured flags, read the board. 14 15 ⚠️ **Submit in batches.** A round hands a working exploit hundreds of 16 flags, and one request per flag is not a slow client — it is an 17 unplayable event on a single-process platform. The rate limit counts 18 requests, so a team's whole round fits in one or two calls. 19 """ 20 21 def __init__(self, http: BaseHttpClient) -> None: 22 self._http = http 23 24 def submit_flags(self, competition_id: str, flags: builtins.list[str]) -> AwdFlagSubmitResponse: 25 """Submit captured flags; get one verdict per value, in order. 26 27 Results are **positional**: a value the caller cannot match back 28 (a typo, a truncated read) still occupies its slot, so a client 29 may zip the reply against what it sent. 30 31 Resubmitting is free — a capture is keyed by (attacker, victim, 32 service, round), so a retried batch reports ``already_captured`` 33 rather than scoring twice. 34 """ 35 resp = self._http.request( 36 "POST", 37 f"/competitions/{competition_id}/awd/flags", 38 json={"flags": flags}, 39 ) 40 _raise_for_status(resp) 41 return AwdFlagSubmitResponse.model_validate(resp.json()) 42 43 def board(self, competition_id: str, *, tick: int | None = None) -> AwdBoardResponse: 44 """One round's standings, plus each team's service matrix. 45 46 Defaults to the **last settled** round, not the one in progress: 47 settlement runs at a round's end, so the current round has no 48 scores and asking for it renders an empty board. Pass ``tick`` 49 for history — a settled round's rows never change again. 50 """ 51 params = {} if tick is None else {"tick": tick} 52 resp = self._http.request("GET", f"/competitions/{competition_id}/awd/board", params=params) 53 _raise_for_status(resp) 54 return AwdBoardResponse.model_validate(resp.json()) 55 56 def match(self, competition_id: str) -> AwdMatchStatus: 57 """The match's clock and rules — which round it is, and when it ends. 58 59 Public, like the board. ``tick_ends_at`` comes from the server 60 rather than being derived from ``starts_at`` + ``tick_seconds``: 61 the round arithmetic lives in exactly one place, and a client 62 that recomputed it would be the copy that disagrees. 63 64 ``current_tick`` is ``None`` before the first round and after the 65 last — not clamped, because "no round is in progress" is a real 66 state and a submission made then is judged against nothing. 67 """ 68 resp = self._http.request("GET", f"/competitions/{competition_id}/awd/match") 69 _raise_for_status(resp) 70 return AwdMatchStatus.model_validate(resp.json())
13class AwdResource: 14 """A classic-AWD match: submit captured flags, read the board. 15 16 ⚠️ **Submit in batches.** A round hands a working exploit hundreds of 17 flags, and one request per flag is not a slow client — it is an 18 unplayable event on a single-process platform. The rate limit counts 19 requests, so a team's whole round fits in one or two calls. 20 """ 21 22 def __init__(self, http: BaseHttpClient) -> None: 23 self._http = http 24 25 def submit_flags(self, competition_id: str, flags: builtins.list[str]) -> AwdFlagSubmitResponse: 26 """Submit captured flags; get one verdict per value, in order. 27 28 Results are **positional**: a value the caller cannot match back 29 (a typo, a truncated read) still occupies its slot, so a client 30 may zip the reply against what it sent. 31 32 Resubmitting is free — a capture is keyed by (attacker, victim, 33 service, round), so a retried batch reports ``already_captured`` 34 rather than scoring twice. 35 """ 36 resp = self._http.request( 37 "POST", 38 f"/competitions/{competition_id}/awd/flags", 39 json={"flags": flags}, 40 ) 41 _raise_for_status(resp) 42 return AwdFlagSubmitResponse.model_validate(resp.json()) 43 44 def board(self, competition_id: str, *, tick: int | None = None) -> AwdBoardResponse: 45 """One round's standings, plus each team's service matrix. 46 47 Defaults to the **last settled** round, not the one in progress: 48 settlement runs at a round's end, so the current round has no 49 scores and asking for it renders an empty board. Pass ``tick`` 50 for history — a settled round's rows never change again. 51 """ 52 params = {} if tick is None else {"tick": tick} 53 resp = self._http.request("GET", f"/competitions/{competition_id}/awd/board", params=params) 54 _raise_for_status(resp) 55 return AwdBoardResponse.model_validate(resp.json()) 56 57 def match(self, competition_id: str) -> AwdMatchStatus: 58 """The match's clock and rules — which round it is, and when it ends. 59 60 Public, like the board. ``tick_ends_at`` comes from the server 61 rather than being derived from ``starts_at`` + ``tick_seconds``: 62 the round arithmetic lives in exactly one place, and a client 63 that recomputed it would be the copy that disagrees. 64 65 ``current_tick`` is ``None`` before the first round and after the 66 last — not clamped, because "no round is in progress" is a real 67 state and a submission made then is judged against nothing. 68 """ 69 resp = self._http.request("GET", f"/competitions/{competition_id}/awd/match") 70 _raise_for_status(resp) 71 return AwdMatchStatus.model_validate(resp.json())
A classic-AWD match: submit captured flags, read the board.
⚠️ Submit in batches. A round hands a working exploit hundreds of flags, and one request per flag is not a slow client — it is an unplayable event on a single-process platform. The rate limit counts requests, so a team's whole round fits in one or two calls.
25 def submit_flags(self, competition_id: str, flags: builtins.list[str]) -> AwdFlagSubmitResponse: 26 """Submit captured flags; get one verdict per value, in order. 27 28 Results are **positional**: a value the caller cannot match back 29 (a typo, a truncated read) still occupies its slot, so a client 30 may zip the reply against what it sent. 31 32 Resubmitting is free — a capture is keyed by (attacker, victim, 33 service, round), so a retried batch reports ``already_captured`` 34 rather than scoring twice. 35 """ 36 resp = self._http.request( 37 "POST", 38 f"/competitions/{competition_id}/awd/flags", 39 json={"flags": flags}, 40 ) 41 _raise_for_status(resp) 42 return AwdFlagSubmitResponse.model_validate(resp.json())
Submit captured flags; get one verdict per value, in order.
Results are positional: a value the caller cannot match back (a typo, a truncated read) still occupies its slot, so a client may zip the reply against what it sent.
Resubmitting is free — a capture is keyed by (attacker, victim,
service, round), so a retried batch reports already_captured
rather than scoring twice.
44 def board(self, competition_id: str, *, tick: int | None = None) -> AwdBoardResponse: 45 """One round's standings, plus each team's service matrix. 46 47 Defaults to the **last settled** round, not the one in progress: 48 settlement runs at a round's end, so the current round has no 49 scores and asking for it renders an empty board. Pass ``tick`` 50 for history — a settled round's rows never change again. 51 """ 52 params = {} if tick is None else {"tick": tick} 53 resp = self._http.request("GET", f"/competitions/{competition_id}/awd/board", params=params) 54 _raise_for_status(resp) 55 return AwdBoardResponse.model_validate(resp.json())
One round's standings, plus each team's service matrix.
Defaults to the last settled round, not the one in progress:
settlement runs at a round's end, so the current round has no
scores and asking for it renders an empty board. Pass tick
for history — a settled round's rows never change again.
57 def match(self, competition_id: str) -> AwdMatchStatus: 58 """The match's clock and rules — which round it is, and when it ends. 59 60 Public, like the board. ``tick_ends_at`` comes from the server 61 rather than being derived from ``starts_at`` + ``tick_seconds``: 62 the round arithmetic lives in exactly one place, and a client 63 that recomputed it would be the copy that disagrees. 64 65 ``current_tick`` is ``None`` before the first round and after the 66 last — not clamped, because "no round is in progress" is a real 67 state and a submission made then is judged against nothing. 68 """ 69 resp = self._http.request("GET", f"/competitions/{competition_id}/awd/match") 70 _raise_for_status(resp) 71 return AwdMatchStatus.model_validate(resp.json())
The match's clock and rules — which round it is, and when it ends.
Public, like the board. tick_ends_at comes from the server
rather than being derived from starts_at + tick_seconds:
the round arithmetic lives in exactly one place, and a client
that recomputed it would be the copy that disagrees.
current_tick is None before the first round and after the
last — not clamped, because "no round is in progress" is a real
state and a submission made then is judged against nothing.