// Code to read HTTP data (port of h11/_readers.py).
//
// Each reader takes a ReceiveBuffer and either:
// 1) consumes some of it and returns an Event
// 2) raises a LocalProtocolError
// 3) returns None, meaning "I need more data"
//
// Body readers additionally know what to do on EOF (`read_eof`); for the
// other readers EOF simply produces a ConnectionClosed event.
///|
priv struct ContentLengthReader {
length : Int64
/// The Content-Length as written by the peer, for error messages.
declared : String
mut remaining : Int64
}
///|
/// Where a chunked body reader is. Python h11 tracks this with three
/// independent fields (`_bytes_in_chunk`, `_bytes_to_discard`,
/// `_reading_trailer`) whose combinations are mostly meaningless; here each
/// phase carries exactly the data it needs.
priv enum ChunkPhase {
/// Waiting for a chunk-size line.
ChunkHeader
/// Inside a chunk with `remaining` (> 0) bytes of data still to come.
/// `started` is false until some of the chunk's data has been returned.
ChunkData(remaining~ : Int64, started~ : Bool)
/// After a chunk's data: these bytes of its CRLF still have to be matched
/// and thrown away.
ChunkFooter(Bytes)
/// After the last (zero-size) chunk: reading the trailer section.
Trailer
}
///|
priv struct ChunkedReader {
mut phase : ChunkPhase
}
///|
priv enum Reader {
IdleClient
SendResponseServer
ExpectNothing
ContentLength(ContentLengthReader)
Chunked(ChunkedReader)
Http10
}
///|
/// READERS[(role, state)] for the non-body states.
fn reader_for_state(role : Role, state : State) -> Reader? {
match (role, state) {
(Client, Idle) => Some(IdleClient)
(Server, Idle | SendResponse) => Some(SendResponseServer)
(_, Done | MustClose | Closed) => Some(ExpectNothing)
_ => None
}
}
///|
/// READERS[SEND_BODY][framing](*args)
fn reader_for_framing(framing : Framing) -> Reader {
match framing {
ContentLength(length, declared~) => {
// Python formats the parsed integer, so drop leading zeros (but keep
// all the digits of values too large for an Int64).
let declared = match declared {
Some(d) => {
let mut i = 0
while i < d.length() - 1 && d[i] == b'0' {
i += 1
}
@utf8.decode_lossy(d[i:])
}
None => length.to_string()
}
ContentLength({ length, declared, remaining: length, })
}
Chunked => Chunked(ChunkedReader::new())
Http10 => Http10
}
}
///|
fn ChunkedReader::new() -> ChunkedReader {
{ phase: ChunkHeader, }
}
///|
fn Reader::read(
self : Reader,
buf : ReceiveBuffer,
) -> Event? raise ProtocolError {
match self {
IdleClient => maybe_read_from_idle_client(buf)
SendResponseServer => maybe_read_from_send_response_server(buf)
ExpectNothing => {
if !buf.is_empty() {
raise local_error("Got data when expecting EOF")
}
None
}
ContentLength(r) => r.read(buf)
Chunked(r) => r.read(buf)
Http10 =>
match buf.maybe_extract_at_most(999999999L) {
None => None
Some(data) => Some(Data(Data::new(data)))
}
}
}
///|
/// What to do when EOF arrives with an empty buffer. `None` means the reader
/// has no special EOF handling and a `ConnectionClosed` should be produced.
fn Reader::read_eof(self : Reader) -> Event? raise ProtocolError {
match self {
ContentLength(r) =>
raise RemoteProtocolError(
"peer closed connection without sending complete message body " +
"(received \{r.length - r.remaining} bytes, expected \{r.declared})",
error_status_hint=400,
)
Chunked(_) =>
raise RemoteProtocolError(
"peer closed connection without sending complete message body " +
"(incomplete chunked read)",
error_status_hint=400,
)
Http10 => Some(EndOfMessage({ headers: Headers::empty(), }))
IdleClient | SendResponseServer | ExpectNothing => None
}
}
///|
/// Handle obsolete line folding: lines starting with whitespace are
/// continuations of the previous line.
fn obsolete_line_fold(
lines : ArrayView[Bytes],
) -> Array[Bytes] raise ProtocolError {
let out : Array[Bytes] = []
let mut last : @buffer.Buffer? = None
for line in lines {
let mut ws = 0
while ws < line.length() && is_ows(line[ws]) {
ws += 1
}
if ws > 0 {
guard last is Some(buf) else {
raise local_error("continuation line at start of headers")
}
buf.write_byte(b' ')
buf.write_bytesview(line[ws:])
} else {
if last is Some(buf) {
out.push(buf.to_bytes())
}
let buf = @buffer.Buffer(size_hint=line.length())
buf.write_bytes(line)
last = Some(buf)
}
}
if last is Some(buf) {
out.push(buf.to_bytes())
}
out
}
///|
fn decode_header_lines(
lines : ArrayView[Bytes],
) -> Array[(Bytes, Bytes)] raise ProtocolError {
obsolete_line_fold(lines).map(line => {
guard match_header_field(line) is Some((name, value)) else {
raise local_error("illegal header line: \{bytes_repr(line)}")
}
(name.to_owned(), value.to_owned())
})
}
///|
fn maybe_read_from_idle_client(
buf : ReceiveBuffer,
) -> Event? raise ProtocolError {
guard buf.maybe_extract_lines() is Some(lines) else {
if buf.is_next_line_obviously_invalid_request_line() {
raise local_error("illegal request line")
}
return None
}
if lines.is_empty() {
raise local_error("no request line received")
}
guard match_request_line(lines[0]) is Some((meth, target, http_version)) else {
raise local_error("illegal request line: \{bytes_repr(lines[0])}")
}
let headers = normalize_and_validate(
decode_header_lines(lines[1:]),
parsed=true,
)
Some(
Request(
Request::make(
meth.to_owned(),
target.to_owned(),
headers,
http_version.to_owned(),
),
),
)
}
///|
fn maybe_read_from_send_response_server(
buf : ReceiveBuffer,
) -> Event? raise ProtocolError {
guard buf.maybe_extract_lines() is Some(lines) else {
if buf.is_next_line_obviously_invalid_request_line() {
raise local_error("illegal request line")
}
return None
}
if lines.is_empty() {
raise local_error("no response line received")
}
guard match_status_line(lines[0]) is Some((http_version, status_code, reason)) else {
raise local_error("illegal status line: \{bytes_repr(lines[0])}")
}
let reason = match reason {
None => b""
Some(r) => r.to_owned()
}
let headers = normalize_and_validate(
decode_header_lines(lines[1:]),
parsed=true,
)
let http_version = http_version.to_owned()
if status_code < 200 {
Some(
InformationalResponse(
InformationalResponse::make(status_code, headers, http_version, reason),
),
)
} else {
Some(Response(Response::make(status_code, headers, http_version, reason)))
}
}
///|
fn ContentLengthReader::read(
self : ContentLengthReader,
buf : ReceiveBuffer,
) -> Event? {
if self.remaining == 0L {
return Some(EndOfMessage({ headers: Headers::empty(), }))
}
guard buf.maybe_extract_at_most(self.remaining) is Some(data) else {
return None
}
self.remaining -= data.length().to_int64()
Some(Data(Data::new(data)))
}
///|
fn ChunkedReader::read(
self : ChunkedReader,
buf : ReceiveBuffer,
) -> Event? raise ProtocolError {
for ;; {
match self.phase {
Trailer => {
guard buf.maybe_extract_lines() is Some(lines) else { return None }
return Some(
EndOfMessage({
headers: normalize_and_validate(
decode_header_lines(lines),
parsed=true,
),
}),
)
}
ChunkFooter(expected) => {
guard buf.maybe_extract_at_most(expected.length().to_int64())
is Some(data) else {
return None
}
if data[:] != expected[:data.length()] {
raise local_error(
"malformed chunk footer: \{bytes_repr(data)} (expected \{bytes_repr(expected)})",
)
}
if data.length() < expected.length() {
self.phase = ChunkFooter(expected[data.length():].to_owned())
return None
}
// footer done; go on to the next chunk header
self.phase = ChunkHeader
}
ChunkHeader => {
guard buf.maybe_extract_next_line() is Some(chunk_header) else {
return None
}
guard match_chunk_header(chunk_header) is Some(size) else {
raise local_error("illegal chunk header: \{bytes_repr(chunk_header)}")
}
// XX FIXME: we discard chunk extensions. Does anyone care?
self.phase = if size == 0L {
Trailer
} else {
ChunkData(remaining=size, started=false)
}
}
ChunkData(remaining~, started~) => {
guard buf.maybe_extract_at_most(remaining) is Some(data) else {
return None
}
let left = remaining - data.length().to_int64()
self.phase = if left == 0L {
ChunkFooter(b"\r\n")
} else {
ChunkData(remaining=left, started=true)
}
return Some(
Data(Data::new(data, chunk_start=!started, chunk_end=left == 0L)),
)
}
}
}
}