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())
class AwdResource:
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.

AwdResource(http: ctfy.sdk.base.BaseHttpClient)
22    def __init__(self, http: BaseHttpClient) -> None:
23        self._http = http
def submit_flags( self, competition_id: str, flags: list[str]) -> ctfy.server.models.AwdFlagSubmitResponse:
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.

def board( self, competition_id: str, *, tick: int | None = None) -> ctfy.server.models.AwdBoardResponse:
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.

def match(self, competition_id: str) -> ctfy.server.models.AwdMatchStatus:
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.