///|
pub(all) struct HttpHeader {
name : String
value : String
} derive(Eq, @debug.Debug)
///|
pub(all) struct HttpMessage {
headers : Array[HttpHeader]
body : Bytes
} derive(Eq, @debug.Debug)
///|
pub(all) struct HttpIntegrityResult {
field_name : String
combined_value : String
occurrences : Int
verification : Verification
} derive(Eq, @debug.Debug)
///|
fn digest_header_name(target : IntegrityTarget) -> String {
match target {
Content => "Content-Digest"
Representation => "Repr-Digest"
}
}
///|
fn want_digest_header_name(target : IntegrityTarget) -> String {
match target {
Content => "Want-Content-Digest"
Representation => "Want-Repr-Digest"
}
}
///|
fn header_values(headers : Array[HttpHeader], name : String) -> Array[String] {
let expected = ascii_lower(name)
let values : Array[String] = []
for header in headers {
if ascii_lower(header.name) == expected {
values.push(header.value)
}
}
values
}
///|
fn combine_field_lines(values : Array[String]) -> String {
let output = StringBuilder()
for i = 0; i < values.length(); i = i + 1 {
if i > 0 {
output.write_string(", ")
}
output.write_string(values[i])
}
output.to_string()
}
///|
/// Verify digest headers from an HTTP adapter without depending on a specific
/// server framework. Repeated field lines are combined in wire order.
pub fn verify_http_message(
message : HttpMessage,
target : IntegrityTarget,
policy? : VerificationPolicy = VerificationPolicy::default(),
) -> HttpIntegrityResult {
let field_name = digest_header_name(target)
let values = header_values(message.headers, field_name)
if values.length() == 0 {
return {
field_name,
combined_value: "",
occurrences: 0,
verification: {
target,
decision: MissingField,
algorithm: "",
code: "MISSING_FIELD",
checked: 0,
ignored: 0,
},
}
}
let combined_value = combine_field_lines(values)
{
field_name,
combined_value,
occurrences: values.length(),
verification: verify_digest_field(
target,
combined_value,
message.body,
policy,
),
}
}
///|
/// Generate and set Content-Digest or Repr-Digest, replacing field lines with
/// the same case-insensitive name while preserving all unrelated headers.
pub fn apply_digest_header(
message : HttpMessage,
target : IntegrityTarget,
algorithms? : Array[String] = ["sha-256", "sha-512"],
) -> HttpMessage raise DigestFieldError {
let name = digest_header_name(target)
let expected = ascii_lower(name)
let headers : Array[HttpHeader] = []
for header in message.headers {
if ascii_lower(header.name) != expected {
headers.push(header)
}
}
headers.push({
name,
value: digest_field_for(message.body, algorithms).to_string(),
})
{ headers, body: message.body, }
}
///|
/// Parse Want-Content-Digest or Want-Repr-Digest headers and choose an
/// algorithm. Repeated field lines are combined before parsing.
pub fn negotiate_http_digest(
headers : Array[HttpHeader],
target : IntegrityTarget,
supported? : Array[String] = ["sha-512", "sha-256"],
) -> NegotiationResult raise DigestFieldError {
let values = header_values(headers, want_digest_header_name(target))
if values.length() == 0 {
return { algorithm: None, acceptable: 0, unsupported: 0, prohibited: 0, }
}
negotiate_digest_algorithm(
parse_preference_field(combine_field_lines(values)),
supported,
)
}