Implement all HTTP preconditions in GetObject/HeadObject #967

Merged
lx merged 3 commits from fix-804 into main 2025-02-19 17:31:27 +00:00
Showing only changes of commit 1a8f74fc94 - Show all commits

View file

@ -9,8 +9,8 @@ use futures::future;
use futures::stream::{self, Stream, StreamExt}; use futures::stream::{self, Stream, StreamExt};
use http::header::{ use http::header::{
ACCEPT_RANGES, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LANGUAGE, ACCEPT_RANGES, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LANGUAGE,
CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, EXPIRES, IF_MODIFIED_SINCE, IF_NONE_MATCH, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, EXPIRES, IF_MATCH, IF_MODIFIED_SINCE,
LAST_MODIFIED, RANGE, IF_NONE_MATCH, IF_UNMODIFIED_SINCE, LAST_MODIFIED, RANGE,
}; };
use hyper::{Request, Response, StatusCode}; use hyper::{Request, Response, StatusCode};
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -115,42 +115,67 @@ fn getobject_override_headers(
Ok(()) Ok(())
} }
fn try_answer_cached( fn handle_http_precondition(
version: &ObjectVersion, version: &ObjectVersion,
version_meta: &ObjectVersionMeta, version_meta: &ObjectVersionMeta,
req: &Request<()>, req: &Request<()>,
) -> Option<Response<ResBody>> { ) -> Result<Option<Response<ResBody>>, Error> {
if let Some(if_match) = req.headers().get(IF_MATCH) {
let if_match = if_match.to_str()?;
let expected = format!("\"{}\"", version_meta.etag);
let found = if_match
.split(',')
.map(str::trim)
.any(|etag| etag == expected || etag == "\"*\"");
if !found {
return Ok(Some(
Response::builder()
.status(StatusCode::PRECONDITION_FAILED)
.body(empty_body())
.unwrap(),
));
}
}
// <trinity> It is possible, and is even usually the case, [that both If-None-Match and // <trinity> It is possible, and is even usually the case, [that both If-None-Match and
// If-Modified-Since] are present in a request. In this situation If-None-Match takes // If-Modified-Since] are present in a request. In this situation If-None-Match takes
// precedence and If-Modified-Since is ignored (as per 6.Precedence from rfc7232). The rational // precedence and If-Modified-Since is ignored (as per 6.Precedence from rfc7232). The rational
// being that etag based matching is more accurate, it has no issue with sub-second precision // being that etag based matching is more accurate, it has no issue with sub-second precision
// for instance (in case of very fast updates) // for instance (in case of very fast updates)
let object_date = UNIX_EPOCH + Duration::from_millis(version.timestamp);
let cached = if let Some(none_match) = req.headers().get(IF_NONE_MATCH) { let cached = if let Some(none_match) = req.headers().get(IF_NONE_MATCH) {
let none_match = none_match.to_str().ok()?; let none_match = none_match.to_str()?;
let expected = format!("\"{}\"", version_meta.etag); let expected = format!("\"{}\"", version_meta.etag);
let found = none_match let found = none_match
.split(',') .split(',')
.map(str::trim) .map(str::trim)
.any(|etag| etag == expected || etag == "\"*\""); .any(|etag| etag == expected || etag == "\"*\"");
found found
} else if let Some(unmodified_since) = req.headers().get(IF_UNMODIFIED_SINCE) {
let unmodified_since = unmodified_since.to_str()?;
let unmodified_since =
httpdate::parse_http_date(unmodified_since).ok_or_bad_request("invalid http date")?;
object_date <= unmodified_since
} else if let Some(modified_since) = req.headers().get(IF_MODIFIED_SINCE) { } else if let Some(modified_since) = req.headers().get(IF_MODIFIED_SINCE) {
let modified_since = modified_since.to_str().ok()?; let modified_since = modified_since.to_str()?;
let client_date = httpdate::parse_http_date(modified_since).ok()?; let modified_since =
let server_date = UNIX_EPOCH + Duration::from_millis(version.timestamp); httpdate::parse_http_date(modified_since).ok_or_bad_request("invalid http date")?;
client_date >= server_date let object_date = UNIX_EPOCH + Duration::from_millis(version.timestamp);
object_date > modified_since
} else { } else {
false false
}; };
if cached { if cached {
Some( Ok(Some(
Response::builder() Response::builder()
.status(StatusCode::NOT_MODIFIED) .status(StatusCode::NOT_MODIFIED)
.body(empty_body()) .body(empty_body())
.unwrap(), .unwrap(),
) ))
} else { } else {
None Ok(None)
} }
} }
@ -196,8 +221,8 @@ pub async fn handle_head_without_ctx(
_ => unreachable!(), _ => unreachable!(),
}; };
if let Some(cached) = try_answer_cached(object_version, version_meta, req) { if let Some(res) = handle_http_precondition(object_version, version_meta, req)? {
return Ok(cached); return Ok(res);
} }
let (encryption, headers) = let (encryption, headers) =
@ -318,8 +343,8 @@ pub async fn handle_get_without_ctx(
ObjectVersionData::FirstBlock(meta, _) => meta, ObjectVersionData::FirstBlock(meta, _) => meta,
}; };
if let Some(cached) = try_answer_cached(last_v, last_v_meta, req) { if let Some(res) = handle_http_precondition(last_v, last_v_meta, req)? {
return Ok(cached); return Ok(res);
} }
let (enc, headers) = let (enc, headers) =