//! 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 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, } /// 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: " HTTP/1.1\r\nHost: \r\n", each header as ": \r\n", // "Content-Length: \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 { // `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 { // 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, 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 { // 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, 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!() }