use std::io::{self, SeekFrom};
use std::time::Duration;
use axum::body::Body;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use bytes::Bytes;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use tokio::sync::{OwnedSemaphorePermit, mpsc};
use crate::artifacts::content::PinnedFile;
use crate::http::limits::{self, Limits};
use crate::policy::Decision;
const CHUNK_BYTES: usize = 64 * 1024;
pub struct Authorized {
blocklist_revision: Option<u64>,
_private: (),
}
impl Authorized {
pub fn from_decision(
decision: Decision,
blocklist_revision: Option<u64>,
) -> Option<Authorized> {
match decision {
Decision::Allow => Some(Authorized {
blocklist_revision,
_private: (),
}),
Decision::Unavailable | Decision::Deny(_) | Decision::Hold { .. } => None,
}
}
pub fn blocklist_revision(&self) -> Option<u64> {
self.blocklist_revision
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ByteRange {
pub start: u64,
pub end: u64,
}
impl ByteRange {
pub fn length(&self) -> u64 {
self.end.saturating_sub(self.start).saturating_add(1)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RangeRequest {
Whole,
One(ByteRange),
Unsatisfiable,
}
pub fn parse_range(header: Option<&str>, total: u64) -> RangeRequest {
let Some(value) = header else {
return RangeRequest::Whole;
};
let Some(spec) = value.trim().strip_prefix("bytes=") else {
return RangeRequest::Whole;
};
if spec.contains(',') {
return RangeRequest::Whole;
}
let Some((first, last)) = spec.split_once('-') else {
return RangeRequest::Whole;
};
let (first, last) = (first.trim(), last.trim());
let range = match (first.is_empty(), last.is_empty()) {
(true, false) => {
let Ok(suffix) = last.parse::<u64>() else {
return RangeRequest::Whole;
};
if suffix == 0 {
return RangeRequest::Unsatisfiable;
}
let suffix = suffix.min(total);
ByteRange {
start: total - suffix,
end: total.saturating_sub(1),
}
}
(false, true) => {
let Ok(start) = first.parse::<u64>() else {
return RangeRequest::Whole;
};
ByteRange {
start,
end: total.saturating_sub(1),
}
}
(false, false) => {
let (Ok(start), Ok(end)) = (first.parse::<u64>(), last.parse::<u64>()) else {
return RangeRequest::Whole;
};
ByteRange {
start,
end: end.min(total.saturating_sub(1)),
}
}
(true, true) => return RangeRequest::Whole,
};
if total == 0 || range.start >= total || range.start > range.end {
return RangeRequest::Unsatisfiable;
}
RangeRequest::One(range)
}
pub struct ArtifactResponse {
status: StatusCode,
total_length: u64,
range: Option<ByteRange>,
body: Option<PinnedFile>,
permit: Option<OwnedSemaphorePermit>,
write_idle: Duration,
lifetime: Duration,
}
impl ArtifactResponse {
pub fn with_body(
auth: Authorized,
file: PinnedFile,
range: Option<ByteRange>,
total_length: u64,
) -> ArtifactResponse {
tracing::debug!(
blocklist_revision = ?auth.blocklist_revision(),
bytes = range.map_or(total_length, |range| range.length()),
"authorized an artifact response"
);
ArtifactResponse {
status: if range.is_some() {
StatusCode::PARTIAL_CONTENT
} else {
StatusCode::OK
},
total_length,
range,
body: Some(file),
permit: None,
write_idle: limits::WRITE_IDLE,
lifetime: limits::RESPONSE_LIFETIME,
}
}
pub fn head_only(auth: Authorized, total_length: u64) -> ArtifactResponse {
tracing::debug!(
blocklist_revision = ?auth.blocklist_revision(),
"authorized an artifact HEAD response"
);
ArtifactResponse {
status: StatusCode::OK,
total_length,
range: None,
body: None,
permit: None,
write_idle: limits::WRITE_IDLE,
lifetime: limits::RESPONSE_LIFETIME,
}
}
pub fn range_not_satisfiable(auth: Authorized, total_length: u64) -> ArtifactResponse {
tracing::debug!(
blocklist_revision = ?auth.blocklist_revision(),
"authorized an artifact response, then found the requested range unsatisfiable"
);
ArtifactResponse {
status: StatusCode::RANGE_NOT_SATISFIABLE,
total_length,
range: None,
body: None,
permit: None,
write_idle: limits::WRITE_IDLE,
lifetime: limits::RESPONSE_LIFETIME,
}
}
pub fn under(mut self, limits: &Limits, permit: OwnedSemaphorePermit) -> ArtifactResponse {
self.permit = Some(permit);
self.write_idle = limits.write_idle();
self.lifetime = limits.response_lifetime();
self
}
}
impl IntoResponse for ArtifactResponse {
fn into_response(self) -> Response {
let unsatisfiable = self.status == StatusCode::RANGE_NOT_SATISFIABLE;
let length = if unsatisfiable {
0
} else {
self.range.map_or(self.total_length, |range| range.length())
};
let content_range = match (unsatisfiable, self.range) {
(true, _) => Some(format!("bytes */{}", self.total_length)),
(false, Some(range)) => Some(format!(
"bytes {}-{}/{}",
range.start, range.end, self.total_length
)),
(false, None) => None,
};
let body = match self.body {
None => Body::empty(),
Some(file) => {
let start = self.range.map_or(0, |range| range.start);
file_body(
file,
start,
length,
self.write_idle,
self.lifetime,
self.permit,
)
}
};
let mut response = Response::new(body);
*response.status_mut() = self.status;
let headers = response.headers_mut();
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"),
);
headers.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
if let Ok(value) = HeaderValue::from_str(&length.to_string()) {
headers.insert(header::CONTENT_LENGTH, value);
}
if let Some(content_range) = content_range
&& let Ok(value) = HeaderValue::from_str(&content_range)
{
headers.insert(header::CONTENT_RANGE, value);
}
response
}
}
fn file_body(
file: PinnedFile,
start: u64,
length: u64,
write_idle: Duration,
lifetime: Duration,
permit: Option<OwnedSemaphorePermit>,
) -> Body {
let (chunks, receiver) = mpsc::channel::<Result<Bytes, io::Error>>(1);
tokio::spawn(async move {
let _permit = permit;
let mut file = file;
if tokio::time::timeout(
lifetime,
produce(&mut file, start, length, write_idle, &chunks),
)
.await
.is_err()
{
tracing::info!(
seconds = lifetime.as_secs(),
"an artifact response reached its lifetime limit; releasing its pin and permit"
);
}
});
Body::from_stream(futures_util::stream::unfold(
receiver,
|mut receiver| async move { receiver.recv().await.map(|chunk| (chunk, receiver)) },
))
}
async fn produce(
file: &mut PinnedFile,
start: u64,
length: u64,
write_idle: Duration,
chunks: &mpsc::Sender<Result<Bytes, io::Error>>,
) {
if let Err(err) = file.file_mut().seek(SeekFrom::Start(start)).await {
let _ = chunks.send(Err(err)).await;
return;
}
let mut remaining = length;
while remaining > 0 {
let wanted = remaining.min(CHUNK_BYTES as u64) as usize;
let mut buffer = vec![0u8; wanted];
let read = match file.file_mut().read(&mut buffer).await {
Ok(0) => return,
Ok(read) => read,
Err(err) => {
let _ = chunks.send(Err(err)).await;
return;
}
};
buffer.truncate(read);
remaining -= read as u64;
match tokio::time::timeout(write_idle, chunks.send(Ok(Bytes::from(buffer)))).await {
Ok(Ok(())) => {}
Err(_elapsed) => {
tracing::info!(
seconds = write_idle.as_secs(),
"an artifact response hit the downstream write-idle limit; releasing its \
pin and permit"
);
return;
}
Ok(Err(_closed)) => return,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::DenyReason;
#[test]
fn only_an_allow_mints_a_witness() {
assert!(Authorized::from_decision(Decision::Allow, Some(7)).is_some());
assert!(
Authorized::from_decision(Decision::Deny(DenyReason::BlockedDigest), Some(7)).is_none()
);
assert!(
Authorized::from_decision(
Decision::Hold {
eligible_at_micros: 1
},
Some(7)
)
.is_none()
);
assert!(Authorized::from_decision(Decision::Unavailable, None).is_none());
}
#[test]
fn a_single_range_is_honoured_and_several_are_ignored() {
assert_eq!(parse_range(None, 10), RangeRequest::Whole);
assert_eq!(
parse_range(Some("bytes=2-5"), 10),
RangeRequest::One(ByteRange { start: 2, end: 5 })
);
assert_eq!(
parse_range(Some("bytes=4-"), 10),
RangeRequest::One(ByteRange { start: 4, end: 9 })
);
assert_eq!(
parse_range(Some("bytes=-3"), 10),
RangeRequest::One(ByteRange { start: 7, end: 9 })
);
assert_eq!(
parse_range(Some("bytes=0-99"), 10),
RangeRequest::One(ByteRange { start: 0, end: 9 }),
"an end past the body is clamped, which is ordinary 206 semantics"
);
assert_eq!(
parse_range(Some("bytes=0-1,4-5"), 10),
RangeRequest::Whole,
"SPEC §9: ignore unsupported multiple ranges and return the complete body"
);
assert_eq!(parse_range(Some("items=0-1"), 10), RangeRequest::Whole);
}
#[test]
fn a_range_past_the_end_is_unsatisfiable() {
assert_eq!(
parse_range(Some("bytes=10-12"), 10),
RangeRequest::Unsatisfiable
);
assert_eq!(
parse_range(Some("bytes=5-2"), 10),
RangeRequest::Unsatisfiable
);
assert_eq!(
parse_range(Some("bytes=0-0"), 0),
RangeRequest::Unsatisfiable
);
}
}