use super::HttpPolicyExecutor;
use super::HttpRendered;
use crate::RedactionReason;
use crate::formats::http::BodyCapture;
use crate::formats::http::BodyRenderReason;
use crate::formats::http::BodyRenderStatus;
use crate::formats::http::internal::AdmittedBody;
use crate::formats::http::internal::BoundedLogWriter;
use crate::formats::http::internal::ParsedBody;
use crate::formats::http::internal::content_type;
use crate::formats::http::internal::markers;
impl HttpPolicyExecutor<'_> {
#[must_use]
#[inline]
pub(super) fn invalid_content_type_body() -> ParsedBody {
ParsedBody::new(
markers::INVALID_CONTENT_TYPE.to_string(),
BodyRenderStatus::Redacted(BodyRenderReason::InvalidContentType),
false,
)
}
#[must_use]
pub(super) fn redact_body_with_content_type_and_admission(
&self,
capture: BodyCapture<'_>,
content_type: Option<&str>,
invalid_content_type: bool,
mut admitted: AdmittedBody,
output_limit: usize,
) -> HttpRendered {
if self.policy.is_disabled() {
return self.finish_diagnostic_with_limit(
String::from_utf8_lossy(capture.bytes()).into_owned(),
output_limit,
None,
);
}
let bounded = capture.bytes();
let truncated = capture.is_source_truncated();
let parsed = if invalid_content_type {
Self::invalid_content_type_body()
} else {
match &mut admitted {
AdmittedBody::Json(value) => self.redact_json_value(bounded, value, truncated, output_limit),
AdmittedBody::InvalidJson => Self::invalid_json_body(),
AdmittedBody::Ndjson {
lines,
trailing_newline,
} => self.redact_ndjson_values(lines, *trailing_newline, truncated, output_limit),
AdmittedBody::InvalidNdjson => Self::invalid_ndjson_body(),
AdmittedBody::Multipart(parts) => {
self.redact_body_inner(bounded, content_type, truncated, output_limit, Some(parts))
}
AdmittedBody::Other => self.redact_body_inner(bounded, content_type, truncated, output_limit, None),
}
};
Self::finish_body_redaction(parsed, capture, output_limit)
}
#[must_use]
fn redact_body_inner(
&self,
bounded: &[u8],
content_type: Option<&str>,
truncated: bool,
output_limit: usize,
admitted: Option<&mut crate::formats::http::internal::AdmittedMultipart>,
) -> ParsedBody {
if bounded.is_empty() {
return ParsedBody::new(String::new(), BodyRenderStatus::Empty, false);
}
let content_type = match content_type {
Some(value) => match content_type::parse(value) {
Some(value) => Some(value),
None => return Self::invalid_content_type_body(),
},
None => None,
};
if let Some(content_type::ContentType::Multipart {
boundary,
require_form_data,
}) = &content_type
{
return self.redact_multipart_body(
bounded,
boundary.as_deref(),
*require_form_data,
truncated,
output_limit,
admitted,
);
}
if matches!(&content_type, Some(content_type::ContentType::Ndjson)) {
return if truncated {
ParsedBody::new(
markers::INVALID_OR_TRUNCATED_NDJSON.to_string(),
BodyRenderStatus::Redacted(BodyRenderReason::InvalidOrTruncatedNdjson),
false,
)
} else {
Self::invalid_ndjson_body()
};
}
let trimmed = trim_ascii_whitespace(bounded);
if matches!(&content_type, Some(content_type::ContentType::Json))
|| (content_type.is_none() && matches!(trimmed.first(), Some(b'{') | Some(b'[')))
{
return if truncated {
ParsedBody::new(
markers::INVALID_OR_TRUNCATED_JSON.to_string(),
BodyRenderStatus::Redacted(BodyRenderReason::InvalidOrTruncatedJson),
false,
)
} else {
Self::invalid_json_body()
};
}
if matches!(&content_type, Some(content_type::ContentType::Form)) {
return self.redact_body_form(bounded, truncated, output_limit);
}
self.redact_fallback(
bounded,
matches!(&content_type, Some(content_type::ContentType::Text)),
output_limit,
)
}
#[must_use]
fn finish_body_redaction(parsed: ParsedBody, capture: BodyCapture<'_>, output_limit: usize) -> HttpRendered {
let (parsed_text, status, rendered_truncated) = parsed.into_parts();
let source_truncated = capture.is_source_truncated() || rendered_truncated;
let mut writer = BoundedLogWriter::new(output_limit, source_truncated);
let _ = writer.write_str(&parsed_text);
let output_truncated = rendered_truncated || writer.is_output_truncated();
let reason = if output_truncated {
RedactionReason::OutputLimitReached
} else {
RedactionReason::SourceTruncated
};
let mut operation = writer.finish_operation(reason);
let provenance = match status {
BodyRenderStatus::Redacted(BodyRenderReason::InvalidJson)
| BodyRenderStatus::Redacted(BodyRenderReason::InvalidOrTruncatedJson)
| BodyRenderStatus::Redacted(BodyRenderReason::InvalidNdjson)
| BodyRenderStatus::Redacted(BodyRenderReason::InvalidOrTruncatedNdjson) => {
Some(RedactionReason::InvalidJson)
}
BodyRenderStatus::Redacted(BodyRenderReason::InvalidContentType) => {
Some(RedactionReason::InvalidContentType)
}
BodyRenderStatus::Redacted(BodyRenderReason::UnsupportedMediaType) => {
Some(RedactionReason::UnsupportedContentType)
}
BodyRenderStatus::Redacted(BodyRenderReason::InvalidFormUrlEncoded)
| BodyRenderStatus::Redacted(BodyRenderReason::InvalidOrTruncatedFormUrlEncoded) => {
Some(RedactionReason::InvalidForm)
}
BodyRenderStatus::Redacted(BodyRenderReason::InvalidMultipart)
| BodyRenderStatus::Redacted(BodyRenderReason::TruncatedMultipart) => {
Some(RedactionReason::InvalidMultipart)
}
_ => None,
};
if capture.is_source_truncated() {
operation = operation.with_reason(RedactionReason::SourceTruncated);
}
if let Some(reason) = provenance {
operation = operation.with_reason(reason);
}
HttpRendered::new(operation)
}
}
#[must_use]
pub(super) fn trim_ascii_whitespace(mut bytes: &[u8]) -> &[u8] {
while bytes.first().is_some_and(u8::is_ascii_whitespace) {
bytes = &bytes[1..];
}
while bytes.last().is_some_and(u8::is_ascii_whitespace) {
bytes = &bytes[..bytes.len() - 1];
}
bytes
}