diff --git a/src/api/s3/copy.rs b/src/api/s3/copy.rs index 9ae48807..ff8019e6 100644 --- a/src/api/s3/copy.rs +++ b/src/api/s3/copy.rs @@ -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, }, diff --git a/src/api/s3/lib.rs b/src/api/s3/lib.rs index 4d1d3ef5..83f684f8 100644 --- a/src/api/s3/lib.rs +++ b/src/api/s3/lib.rs @@ -14,7 +14,7 @@ mod list; mod multipart; mod post_object; mod put; -mod website; +pub mod website; mod encryption; mod router; diff --git a/src/api/s3/multipart.rs b/src/api/s3/multipart.rs index 1ee04bc1..d6eb26cb 100644 --- a/src/api/s3/multipart.rs +++ b/src/api/s3/multipart.rs @@ -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, diff --git a/src/api/s3/post_object.rs b/src/api/s3/post_object.rs index 350684da..b9bccae6 100644 --- a/src/api/s3/post_object.rs +++ b/src/api/s3/post_object.rs @@ -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(¶ms)?; + let headers = extract_metadata_headers(¶ms)?; let checksum_algorithm = request_checksum_algorithm(¶ms)?; let expected_checksums = ExpectedChecksums { diff --git a/src/api/s3/put.rs b/src/api/s3/put.rs index 4d866a06..830a7998 100644 --- a/src/api/s3/put.rs +++ b/src/api/s3/put.rs @@ -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, 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) -> Result { +pub(crate) fn extract_metadata_headers( + headers: &HeaderMap, +) -> Result { let mut ret = Vec::new(); // Preserve standard headers @@ -675,6 +678,18 @@ pub(crate) fn get_headers(headers: &HeaderMap) -> Result Result, Error> { let ReqCtx { bucket_params, .. } = ctx; if let Some(website) = bucket_params.website_config.get() { diff --git a/src/garage/tests/s3/website.rs b/src/garage/tests/s3/website.rs index 0cadc388..9a9e29f2 100644 --- a/src/garage/tests/s3/website.rs +++ b/src/garage/tests/s3/website.rs @@ -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() diff --git a/src/web/web_server.rs b/src/web/web_server.rs index 34ba834c..242f7801 100644 --- a/src/web/web_server.rs +++ b/src/web/web_server.rs @@ -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()) }