// Headers are a list of (name, value) pairs where names are compared
// case-insensitively, duplicates are allowed and order matters. We store the
// original casing (to preserve it over the wire) together with the
// lowercased name (for comparisons).
///|
/// Maximum number of digits accepted in a Content-Length value (allows up to
/// 1 billion TB - 1).
let content_length_max_digits = 20
///|
/// A list-like collection of headers. Iterating yields
/// `(lowercased_name, value)` pairs; `raw_items` yields the names with their
/// original casing.
pub struct Headers {
priv full_items : Array[(Bytes, Bytes, Bytes)]
// Framing facts established during validation, so that body framing never
// has to re-parse (or re-check) the headers.
/// `Transfer-Encoding: chunked` is present (the only coding we accept).
priv chunked : Bool
/// The (deduplicated) Content-Length, with the digits as written.
priv content_length : (Int64, Bytes)?
}
///|
/// Normalize and validate a list of `(name, value)` pairs.
///
/// Header names must be tokens and values must be valid field values (no
/// leading/trailing whitespace, NUL, CR or LF). `Content-Length` and
/// `Transfer-Encoding` receive extra checks.
pub fn Headers::new(
items : ArrayView[(Bytes, Bytes)],
) -> Headers raise ProtocolError {
normalize_and_validate(items, parsed=false)
}
///|
/// An empty header list.
pub fn Headers::empty() -> Headers {
{ full_items: [], chunked: false, content_length: None, }
}
///|
pub fn Headers::length(self : Headers) -> Int {
self.full_items.length()
}
///|
pub fn Headers::is_empty(self : Headers) -> Bool {
self.full_items.is_empty()
}
///|
/// The `idx`-th header as a `(lowercased_name, value)` pair.
#alias("_[_]")
pub fn Headers::at(self : Headers, idx : Int) -> (Bytes, Bytes) {
let (_, name, value) = self.full_items[idx]
(name, value)
}
///|
/// The `idx`-th header as a `(lowercased_name, value)` pair, or `None` if
/// `idx` is out of range.
pub fn Headers::get(self : Headers, idx : Int) -> (Bytes, Bytes)? {
match self.full_items.get(idx) {
Some((_, name, value)) => Some((name, value))
None => None
}
}
///|
/// Iterate over `(lowercased_name, value)` pairs.
pub fn Headers::iter(self : Headers) -> Iter[(Bytes, Bytes)] {
self.full_items.iter().map(item => (item.1, item.2))
}
///|
/// All headers as `(lowercased_name, value)` pairs.
pub fn Headers::to_array(self : Headers) -> Array[(Bytes, Bytes)] {
self.full_items.map(item => (item.1, item.2))
}
///|
/// All headers as `(raw_name, value)` pairs, preserving the original casing
/// of the names.
pub fn Headers::raw_items(self : Headers) -> Array[(Bytes, Bytes)] {
self.full_items.map(item => (item.0, item.2))
}
///|
/// Two header lists are equal when their `(lowercased_name, value)` pairs
/// are equal; the original casing is ignored.
pub impl Eq for Headers with fn equal(self, other) {
let n = self.full_items.length()
if n != other.full_items.length() {
return false
}
for i in 0.. {
Repr::tuple([bytes_debug(item.1), bytes_debug(item.2)])
}),
)
}
///|
/// Shared implementation of header normalization. When `parsed` is true the
/// data came out of our own parser, which already validated names and
/// values, so those checks are skipped.
fn normalize_and_validate(
headers : ArrayView[(Bytes, Bytes)],
parsed~ : Bool,
) -> Headers raise ProtocolError {
let new_headers = []
let mut seen_content_length : (Int64, Bytes)? = None
let mut saw_transfer_encoding = false
for header in headers {
let (raw_name, value) = header
if !parsed {
if !match_token(raw_name) {
raise local_error("Illegal header name \{bytes_repr(raw_name)}")
}
if !match_field_value(value) {
raise local_error("Illegal header value \{bytes_repr(value)}")
}
}
let name = ascii_lower(raw_name)
if name == b"content-length" {
let lengths : Map[Bytes, Unit] = Map([])
for length in split_on(value, b',') {
lengths[ascii_strip(length).to_owned()] = ()
}
if lengths.length() != 1 {
raise local_error("conflicting Content-Length headers")
}
guard lengths.keys().head() is Some(value) else {
raise local_error("conflicting Content-Length headers")
}
if value.is_empty() || !value.iter().all(is_digit) {
raise local_error("bad Content-Length")
}
if value.length() > content_length_max_digits {
raise local_error("bad Content-Length")
}
let length = match parse_decimal(value) {
Some(n) => n
None => raise local_error("bad Content-Length")
}
match seen_content_length {
None => {
seen_content_length = Some((length, value))
new_headers.push((raw_name, name, value))
}
Some((_, seen)) =>
if seen != value {
raise local_error("conflicting Content-Length headers")
}
}
} else if name == b"transfer-encoding" {
// "A server that receives a request message with a transfer coding it
// does not understand SHOULD respond with 501 (Not Implemented)."
// https://tools.ietf.org/html/rfc7230#section-3.3.1
if saw_transfer_encoding {
raise local_error(
"multiple Transfer-Encoding headers",
error_status_hint=501,
)
}
// "All transfer-coding names are case-insensitive"
// -- https://tools.ietf.org/html/rfc7230#section-4
let value = ascii_lower(value)
if value != b"chunked" {
raise local_error(
"Only Transfer-Encoding: chunked is supported",
error_status_hint=501,
)
}
saw_transfer_encoding = true
new_headers.push((raw_name, name, value))
} else {
new_headers.push((raw_name, name, value))
}
}
{
full_items: new_headers,
chunked: saw_transfer_encoding,
content_length: seen_content_length,
}
}
///|
/// Collect the comma-separated, case-insensitive values of every header
/// named `name` (which must be lowercase).
///
/// This naive splitting is wrong for headers that allow quoted strings, but
/// fine for the headers we use it on: Connection, Content-Length,
/// Transfer-Encoding (we reject anything but "chunked" anyway) and Expect.
pub fn get_comma_header(headers : Headers, name : Bytes) -> Array[Bytes] {
let out = []
for item in headers.full_items {
let (_, found_name, found_raw_value) = item
if found_name == name {
let lowered = ascii_lower(found_raw_value)
for found_split_value in split_on(lowered, b',') {
let v = ascii_strip(found_split_value)
if !v.is_empty() {
out.push(v.to_owned())
}
}
}
}
out
}
///|
/// Return a copy of `headers` where every header named `name` (lowercase) is
/// replaced by one header per entry of `new_values`, appended at the end with
/// a title-cased name.
pub fn set_comma_header(
headers : Headers,
name : Bytes,
new_values : ArrayView[Bytes],
) -> Headers raise ProtocolError {
let new_headers = []
for item in headers.full_items {
let (found_raw_name, found_name, found_raw_value) = item
if found_name != name {
new_headers.push((found_raw_name, found_raw_value))
}
}
let title = ascii_title(name)
for new_value in new_values {
new_headers.push((title, new_value))
}
normalize_and_validate(new_headers, parsed=false)
}
///|
/// Whether a request carries `Expect: 100-continue` (ignored for HTTP/1.0,
/// as required by RFC 7231 section 5.1.1).
pub fn has_expect_100_continue(request : Request) -> Bool {
if bytes_lt(request.http_version, b"1.1") {
return false
}
get_comma_header(request.headers, b"expect").contains(b"100-continue")
}