blackbull.fault_injection.scenario_h1¶
blackbull.fault_injection.scenario_h1
¶
Programmable HTTP/1.1 wire-level scenario model.
A :class:Scenario is a sequence of typed steps that the
:meth:blackbull.client.HTTP1Client.execute_scenario executor walks
in order against a live socket. This is the client-side half of
the :mod:blackbull.fault_injection toolkit: a programmable client
that drives a target HTTP/1.1 server through deliberate misbehaviour
— slowloris trickle, mid-request idle, abrupt RST, partial reads —
expressed as data, not procedural test code.
The symmetric server-side half (programmable HTTP/2 server emitting
deliberate misbehaviour toward a client) lives in
:mod:blackbull.fault_injection.h2_server.
Use cases:
- Conformance differential testing — Hypothesis generates scenarios
and :mod:
blackbull.fault_injection.oracle_h1compares the target server's response to a reference (e.g. nginx). - Coverage-guided fuzzing — atheris's byte mutations decode into
scenarios via :meth:
Scenario.from_bytes. - External callers — server-library authors, proxy authors, and security researchers driving their server through programmable misbehaviour from a pytest suite.
Two serialisations are supported:
- :meth:
Scenario.to_json/ :meth:Scenario.from_json— JSON Lines, one step per line. Diff-friendly in git; readable when failures are pasted into reports. - :meth:
Scenario.from_bytes— a total opcode-tagged decoder. Every byte string maps to a valid scenario, so atheris's byte-level mutations never crash on input parsing — each mutation produces a distinct execution path against the server.
Abort
dataclass
¶
Hard-close the connection (transport.abort → RST on Linux).
Distinct from the graceful writer.close() / wait_closed()
in HTTP1Client.__aexit__. Subsequent steps short-circuit; the
executor stops walking the scenario after an Abort.
ReadResponse
dataclass
¶
Read one HTTP/1.1 response from the connection.
timeout bounds the entire status-line + headers + body read.
On timeout the executor records the outcome on the
:class:ScenarioResult and does not raise — the caller decides
whether to treat that as a transport-fail or normal outcome.
Scenario
dataclass
¶
A sequence of steps the executor walks against one connection.
from_bytes(raw)
classmethod
¶
Decode arbitrary bytes into a scenario.
Total function: every byte string yields a valid scenario, including the empty string (→ empty scenario). Designed so atheris's coverage-guided byte mutations always produce runnable input — the fuzzer never spends cycles on parser errors.
Encoding:
- The decoder walks
rawleft-to-right. At each position the next byte selects an opcode via% 4(every byte value is therefore a legal opcode tag). - Each opcode then consumes a small payload from the following bytes. If the payload is short (end of input), decoding stops cleanly and the partial scenario is returned.
Opcode layout::
byte % 4 == 0 → SEND
next 2 bytes (big-endian uint16) = length;
next ``length`` bytes = data;
next 1 byte (% len(_BYTE_INTERVAL_TABLE))
→ byte_interval.
byte % 4 == 1 → SLEEP
next 1 byte (% len(_SLEEP_TABLE)) → duration.
byte % 4 == 2 → READ
next 1 byte (% len(_TIMEOUT_TABLE)) → timeout.
byte % 4 == 3 → ABORT
no payload. Remaining bytes are discarded — an
Abort short-circuits execution anyway, so it's the
natural terminator.
Bounded payload sizes (uint16 length) keep individual scenarios well under 64 KiB, which is what we want for per-iteration fuzz throughput.
from_json(src)
classmethod
¶
Parse JSON Lines back to a :class:Scenario.
Skips blank lines so files that end with a trailing newline (the conventional git-friendly shape) parse cleanly.
to_json()
¶
Serialise to JSON Lines: one {"op": ..., ...} per line.
Bytes payloads are base64-encoded so the result round-trips
through stdout / git / json.loads without escape ambiguity.
Round-tripped by :meth:from_json.
well_formed(raw_request, *, response_timeout=5.0)
classmethod
¶
Wrap a complete raw HTTP/1.1 request as a one-shot scenario.
Equivalent to "send these bytes, then read one response".
Used by the legacy diff_*.txt corpus loader and by the
Hypothesis well_formed_scenario_strategy.
ScenarioResult
dataclass
¶
Outcome of one :meth:HTTP1Client.execute_scenario call.
Exactly one of response / exception / timed_out /
aborted is the meaningful field; the others are None /
False. The executor never raises, so callers (differential
test, fuzz harness) categorise on this object instead of writing
try/except boilerplate per scenario.
SendBytes
dataclass
¶
Push raw bytes onto the connection.
byte_interval > 0 transmits one byte at a time with that delay
between bytes — the primitive slowloris-style stall that lets
scenarios express trickled headers or trickled bodies without
dropping to a raw asyncio socket.
Sleep
dataclass
¶
Idle for duration seconds without sending or reading.
Useful for post-headers idle, mid-keep-alive idle, and pre-response stall scenarios where the server is expected to time out and close.
StepOp
¶
Bases: str, Enum
Tag for serialising / decoding a step. str mixin so values
drop straight into JSON without a custom encoder.