Each task's tests were run against a reference at its end state; the end states were replayed from master in order with the gate at each step (650 to 762 tests); each skeleton compiles against its tests and fails them. The reference is kept off this machine. Lessons T27 (every wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
128 lines
4.6 KiB
Rust
128 lines
4.6 KiB
Rust
//! HTTP/1.1 over a connected stream: one request, one response, `Connection: close` (M4a spec,
|
|
//! section 5). The response head is read a byte at a time, so nothing past it is consumed: the
|
|
//! WebSocket handshake reads its frames after it from the same stream.
|
|
|
|
use std::io::{Read, Write};
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
pub const MAX_HEAD: usize = 16 * 1024;
|
|
pub const MAX_BODY: usize = 4 * 1024 * 1024;
|
|
/// The longest a rate limit is waited out, whatever the server says.
|
|
pub const MAX_RATE_WAIT: Duration = Duration::from_secs(60);
|
|
|
|
#[derive(Debug)]
|
|
pub enum HttpError {
|
|
Io(std::io::Error),
|
|
/// The response is not HTTP/1.1 as we read it.
|
|
Protocol(String),
|
|
/// A head or body over its cap.
|
|
TooLarge(&'static str),
|
|
}
|
|
|
|
impl std::fmt::Display for HttpError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
HttpError::Io(e) => write!(f, "{e}"),
|
|
HttpError::Protocol(why) => write!(f, "bad HTTP response: {why}"),
|
|
HttpError::TooLarge(what) => write!(f, "the response {what} is too large"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for HttpError {}
|
|
|
|
impl From<std::io::Error> for HttpError {
|
|
fn from(e: std::io::Error) -> Self {
|
|
HttpError::Io(e)
|
|
}
|
|
}
|
|
|
|
/// A status line and headers.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Head {
|
|
pub status: u16,
|
|
pub headers: Vec<(String, String)>,
|
|
}
|
|
|
|
impl Head {
|
|
/// The first header named `name`, compared case-insensitively.
|
|
pub fn header(&self, name: &str) -> Option<&str> {
|
|
// The value of the first header whose name matches, ignoring ASCII case.
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Response {
|
|
pub head: Head,
|
|
pub body: Vec<u8>,
|
|
}
|
|
|
|
/// Write one request. `host` is the `Host` header; `headers` come after it, then
|
|
/// `Content-Length` when there is a body, then `Connection: close`.
|
|
pub fn write_request(
|
|
stream: &mut dyn Write,
|
|
method: &str,
|
|
host: &str,
|
|
path: &str,
|
|
headers: &[(&str, &str)],
|
|
body: Option<&[u8]>,
|
|
) -> Result<(), HttpError> {
|
|
// Exactly: "<method> <path> HTTP/1.1\r\nHost: <host>\r\n", each header as "<k>: <v>\r\n",
|
|
// "Content-Length: <n>\r\n" when there is a body, "Connection: close\r\n\r\n", then the body.
|
|
// Flush.
|
|
todo!()
|
|
}
|
|
|
|
/// One request and its whole response.
|
|
pub fn request(
|
|
stream: &mut (impl Read + Write),
|
|
method: &str,
|
|
host: &str,
|
|
path: &str,
|
|
headers: &[(&str, &str)],
|
|
body: Option<&[u8]>,
|
|
) -> Result<Response, HttpError> {
|
|
// `write_request`, then `read_head`, then `read_body`.
|
|
todo!()
|
|
}
|
|
|
|
/// The status line and headers, up to and including the blank line, and not a byte more.
|
|
pub fn read_head(stream: &mut dyn Read) -> Result<Head, HttpError> {
|
|
// One byte at a time until CRLF CRLF, never more; over MAX_HEAD is TooLarge("head"); end of
|
|
// stream is Protocol; retry Interrupted. Then: UTF-8; status line "HTTP/1.1" or "HTTP/1.0", a
|
|
// 3-digit status in 100..=599; each header line "name: value" with a non-empty name without
|
|
// spaces, the value trimmed. Anything else is Protocol.
|
|
todo!()
|
|
}
|
|
|
|
/// The body after `head`: chunked, `Content-Length`, or to the end; at most `MAX_BODY`.
|
|
pub fn read_body(stream: &mut dyn Read, head: &Head) -> Result<Vec<u8>, HttpError> {
|
|
// Transfer-Encoding: chunked (any case) -> `read_chunked`. Else Content-Length: parse (else
|
|
// Protocol), over MAX_BODY is TooLarge("body"), then read_exact that many. Else read to the end
|
|
// through `take(MAX_BODY + 1)`; more than MAX_BODY is TooLarge("body").
|
|
todo!()
|
|
}
|
|
|
|
fn read_line(stream: &mut dyn Read, cap: usize) -> Result<String, HttpError> {
|
|
// Bytes up to CRLF (dropped), at most `cap` (else TooLarge("chunk header")); end of stream is
|
|
// Protocol.
|
|
todo!()
|
|
}
|
|
|
|
fn read_chunked(stream: &mut dyn Read) -> Result<Vec<u8>, HttpError> {
|
|
// Loop: a size line (hex, before any ";"), at most 1024 bytes. Size 0: read trailer lines (8
|
|
// KiB each) until an empty one, and return. Otherwise the size must fit in MAX_BODY minus what
|
|
// is already read (else TooLarge("body")), read it, then exactly CRLF (else Protocol).
|
|
todo!()
|
|
}
|
|
|
|
/// How long a 429 asks us to wait, from `X-Ratelimit-Reset`: a Unix time if it is one, else a
|
|
/// number of seconds; never more than `MAX_RATE_WAIT`. One second if the header is missing or bad.
|
|
pub fn rate_limit_wait(head: &Head, now: SystemTime) -> Duration {
|
|
// X-Ratelimit-Reset as u64: above 1_000_000_000 it is a Unix time (wait = it - now, at least 1
|
|
// s), otherwise seconds (at least 1). Missing or not a number: 1 s. Never more than
|
|
// MAX_RATE_WAIT.
|
|
todo!()
|
|
}
|