Merge pull request 'web: implement x-amz-website-redirect-location' (#966) from redirect-location-header into main
All checks were successful
ci/woodpecker/push/debug Pipeline was successful

Reviewed-on: #966
This commit is contained in:
Alex 2025-02-19 16:10:04 +00:00
commit 2191620af5
8 changed files with 66 additions and 11 deletions

View file

@ -28,7 +28,7 @@ use crate::encryption::EncryptionParams;
use crate::error::*;
use crate::get::full_object_byte_stream;
use crate::multipart;
use crate::put::{get_headers, save_stream, ChecksumMode, SaveStreamResult};
use crate::put::{extract_metadata_headers, save_stream, ChecksumMode, SaveStreamResult};
use crate::xml::{self as s3_xml, xmlns_tag};
// -------- CopyObject ---------
@ -73,7 +73,7 @@ pub async fn handle_copy(
let dest_object_meta = ObjectVersionMetaInner {
headers: match req.headers().get("x-amz-metadata-directive") {
Some(v) if v == hyper::header::HeaderValue::from_static("REPLACE") => {
get_headers(req.headers())?
extract_metadata_headers(req.headers())?
}
_ => source_object_meta_inner.into_owned().headers,
},

View file

@ -14,7 +14,7 @@ mod list;
mod multipart;
mod post_object;
mod put;
mod website;
pub mod website;
mod encryption;
mod router;

View file

@ -49,7 +49,7 @@ pub async fn handle_create_multipart_upload(
let upload_id = gen_uuid();
let timestamp = next_timestamp(existing_object.as_ref());
let headers = get_headers(req.headers())?;
let headers = extract_metadata_headers(req.headers())?;
let meta = ObjectVersionMetaInner {
headers,
checksum: None,

View file

@ -24,7 +24,7 @@ use garage_api_common::signature::payload::{verify_v4, Authorization};
use crate::api_server::ResBody;
use crate::encryption::EncryptionParams;
use crate::error::*;
use crate::put::{get_headers, save_stream, ChecksumMode};
use crate::put::{extract_metadata_headers, save_stream, ChecksumMode};
use crate::xml as s3_xml;
pub async fn handle_post_object(
@ -216,7 +216,7 @@ pub async fn handle_post_object(
// if we ever start supporting ACLs, we likely want to map "acl" to x-amz-acl" somewhere
// around here to make sure the rest of the machinery takes our acl into account.
let headers = get_headers(&params)?;
let headers = extract_metadata_headers(&params)?;
let checksum_algorithm = request_checksum_algorithm(&params)?;
let expected_checksums = ExpectedChecksums {

View file

@ -37,6 +37,7 @@ use garage_api_common::signature::checksum::*;
use crate::api_server::{ReqBody, ResBody};
use crate::encryption::EncryptionParams;
use crate::error::*;
use crate::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
const PUT_BLOCKS_MAX_PARALLEL: usize = 3;
@ -62,7 +63,7 @@ pub async fn handle_put(
key: &String,
) -> Result<Response<ResBody>, Error> {
// Retrieve interesting headers from request
let headers = get_headers(req.headers())?;
let headers = extract_metadata_headers(req.headers())?;
debug!("Object headers: {:?}", headers);
let expected_checksums = ExpectedChecksums {
@ -649,7 +650,9 @@ impl Drop for InterruptedCleanup {
// ============ helpers ============
pub(crate) fn get_headers(headers: &HeaderMap<HeaderValue>) -> Result<HeaderList, Error> {
pub(crate) fn extract_metadata_headers(
headers: &HeaderMap<HeaderValue>,
) -> Result<HeaderList, Error> {
let mut ret = Vec::new();
// Preserve standard headers
@ -675,6 +678,18 @@ pub(crate) fn get_headers(headers: &HeaderMap<HeaderValue>) -> Result<HeaderList
std::str::from_utf8(value.as_bytes())?.to_string(),
));
}
if name == X_AMZ_WEBSITE_REDIRECT_LOCATION {
let value = std::str::from_utf8(value.as_bytes())?.to_string();
if !(value.starts_with("/")
|| value.starts_with("http://")
|| value.starts_with("https://"))
{
return Err(Error::bad_request(format!(
"Invalid {X_AMZ_WEBSITE_REDIRECT_LOCATION} header",
)));
}
ret.push((X_AMZ_WEBSITE_REDIRECT_LOCATION.to_string(), value));
}
}
Ok(ret)

View file

@ -1,6 +1,6 @@
use quick_xml::de::from_reader;
use hyper::{Request, Response, StatusCode};
use hyper::{header::HeaderName, Request, Response, StatusCode};
use serde::{Deserialize, Serialize};
use garage_model::bucket_table::*;
@ -11,6 +11,9 @@ use crate::api_server::{ReqBody, ResBody};
use crate::error::*;
use crate::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
pub const X_AMZ_WEBSITE_REDIRECT_LOCATION: HeaderName =
HeaderName::from_static("x-amz-website-redirect-location");
pub async fn handle_get_website(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
let ReqCtx { bucket_params, .. } = ctx;
if let Some(website) = bucket_params.website_config.get() {

View file

@ -11,6 +11,7 @@ use http::{Request, StatusCode};
use http_body_util::BodyExt;
use http_body_util::Full as FullBody;
use hyper::body::Bytes;
use hyper::header::LOCATION;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;
use serde_json::json;
@ -295,6 +296,33 @@ async fn test_website_s3_api() {
);
}
// Test x-amz-website-redirect-location
{
ctx.client
.put_object()
.bucket(&bucket)
.key("test-redirect.html")
.website_redirect_location("https://perdu.com")
.send()
.await
.unwrap();
let req = Request::builder()
.method("GET")
.uri(format!(
"http://127.0.0.1:{}/test-redirect.html",
ctx.garage.web_port
))
.header("Host", format!("{}.web.garage", BCKT_NAME))
.body(Body::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::MOVED_PERMANENTLY);
assert_eq!(resp.headers().get(LOCATION).unwrap(), "https://perdu.com");
}
// Test CORS with an allowed preflight request
{
let req = Request::builder()

View file

@ -7,7 +7,7 @@ use tokio::sync::watch;
use hyper::{
body::Incoming as IncomingBody,
header::{HeaderValue, HOST},
header::{HeaderValue, HOST, LOCATION},
Method, Request, Response, StatusCode,
};
@ -29,6 +29,7 @@ use garage_api_s3::error::{
CommonErrorDerivative, Error as ApiError, OkOrBadRequest, OkOrInternalError,
};
use garage_api_s3::get::{handle_get_without_ctx, handle_head_without_ctx};
use garage_api_s3::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
use garage_model::garage::Garage;
@ -294,7 +295,15 @@ impl WebServer {
{
Ok(Response::builder()
.status(StatusCode::FOUND)
.header("Location", url)
.header(LOCATION, url)
.body(empty_body())
.unwrap())
}
(Ok(ret), _) if ret.headers().contains_key(X_AMZ_WEBSITE_REDIRECT_LOCATION) => {
let redirect_location = ret.headers().get(X_AMZ_WEBSITE_REDIRECT_LOCATION).unwrap();
Ok(Response::builder()
.status(StatusCode::MOVED_PERMANENTLY)
.header(LOCATION, redirect_location)
.body(empty_body())
.unwrap())
}