feat(api|web): follow through metadata-driven redirects
This is an implementation of the GET handler for the `x-amz-website-redirect-location`. This implements the semantics described in https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-page-redirect.html for the S3 API and the web API. Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
This commit is contained in:
parent
647b2dae33
commit
b99aaa8351
7 changed files with 99 additions and 5 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -197,6 +197,17 @@ dependencies = [
|
|||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.5"
|
||||
|
@ -1365,6 +1376,7 @@ dependencies = [
|
|||
"aes-gcm",
|
||||
"argon2",
|
||||
"async-compression",
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
|
|
15
Cargo.nix
15
Cargo.nix
|
@ -34,7 +34,7 @@ args@{
|
|||
ignoreLockHash,
|
||||
}:
|
||||
let
|
||||
nixifiedLockHash = "c0aa85d369b22875a652356862a5810c22838970be9fbec558dd108d5232881d";
|
||||
nixifiedLockHash = "fe0e77c1af963cc04accebb4ddd1607df5bbbaa48ca0b7a325f5e4497e21790e";
|
||||
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
|
||||
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
|
||||
lockHashIgnored = if ignoreLockHash
|
||||
|
@ -349,6 +349,18 @@ in
|
|||
};
|
||||
});
|
||||
|
||||
"registry+https://github.com/rust-lang/crates.io-index".async-recursion."1.1.1" = overridableMkRustCrate (profileName: rec {
|
||||
name = "async-recursion";
|
||||
version = "1.1.1";
|
||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||
src = fetchCratesIo { inherit name version; sha256 = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"; };
|
||||
dependencies = {
|
||||
proc_macro2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.78" { inherit profileName; }).out;
|
||||
quote = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.35" { inherit profileName; }).out;
|
||||
syn = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."2.0.48" { inherit profileName; }).out;
|
||||
};
|
||||
});
|
||||
|
||||
"registry+https://github.com/rust-lang/crates.io-index".async-stream."0.3.5" = overridableMkRustCrate (profileName: rec {
|
||||
name = "async-stream";
|
||||
version = "0.3.5";
|
||||
|
@ -2003,6 +2015,7 @@ in
|
|||
aes_gcm = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aes-gcm."0.10.3" { inherit profileName; }).out;
|
||||
argon2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".argon2."0.5.3" { inherit profileName; }).out;
|
||||
async_compression = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-compression."0.4.6" { inherit profileName; }).out;
|
||||
async_recursion = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-recursion."1.1.1" { profileName = "__noProfile"; }).out;
|
||||
async_trait = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.77" { profileName = "__noProfile"; }).out;
|
||||
base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
|
||||
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
|
||||
|
|
|
@ -68,6 +68,7 @@ quick-xml.workspace = true
|
|||
opentelemetry.workspace = true
|
||||
opentelemetry-prometheus = { workspace = true, optional = true }
|
||||
prometheus = { workspace = true, optional = true }
|
||||
async-recursion = "1.1.1"
|
||||
|
||||
[features]
|
||||
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
|
||||
|
|
|
@ -201,7 +201,10 @@ impl ApiHandler for S3ApiServer {
|
|||
response_content_type,
|
||||
response_expires,
|
||||
};
|
||||
handle_get(ctx, &req, &key, part_number, overrides).await
|
||||
// Redirects are flattened over the S3 API as per:
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-page-redirect.html
|
||||
// > REST endpoint – Amazon S3 doesn't redirect the page request. It returns the requested object.
|
||||
handle_get(ctx, &req, &key, part_number, overrides, true).await
|
||||
}
|
||||
Endpoint::UploadPart {
|
||||
key,
|
||||
|
|
|
@ -280,22 +280,25 @@ pub async fn handle_head_without_ctx(
|
|||
/// Handle GET request
|
||||
pub async fn handle_get(
|
||||
ctx: ReqCtx,
|
||||
req: &Request<impl Body>,
|
||||
req: &Request<impl Body + std::marker::Sync>,
|
||||
key: &str,
|
||||
part_number: Option<u64>,
|
||||
overrides: GetObjectOverrides,
|
||||
flatten_redirect: bool
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
handle_get_without_ctx(ctx.garage, req, ctx.bucket_id, key, part_number, overrides).await
|
||||
handle_get_without_ctx(ctx.garage, req, ctx.bucket_id, key, part_number, overrides, flatten_redirect).await
|
||||
}
|
||||
|
||||
/// Handle GET request
|
||||
#[async_recursion::async_recursion]
|
||||
pub async fn handle_get_without_ctx(
|
||||
garage: Arc<Garage>,
|
||||
req: &Request<impl Body>,
|
||||
req: &Request<impl Body + std::marker::Sync>,
|
||||
bucket_id: Uuid,
|
||||
key: &str,
|
||||
part_number: Option<u64>,
|
||||
overrides: GetObjectOverrides,
|
||||
flatten_redirect: bool
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let object = garage
|
||||
.object_table
|
||||
|
@ -329,6 +332,30 @@ pub async fn handle_get_without_ctx(
|
|||
|
||||
let checksum_mode = checksum_mode(&req);
|
||||
|
||||
if let Some(redirect_hdr) = headers
|
||||
.headers
|
||||
.iter()
|
||||
.find(|(k, _)| k == "x-amz-website-redirect-location")
|
||||
.map(|(_, v)| v)
|
||||
{
|
||||
if flatten_redirect {
|
||||
return handle_get_without_ctx(garage,
|
||||
req,
|
||||
bucket_id,
|
||||
redirect_hdr,
|
||||
part_number,
|
||||
overrides,
|
||||
flatten_redirect
|
||||
).await;
|
||||
} else {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::FOUND)
|
||||
.header("Location", redirect_hdr)
|
||||
.body(empty_body())
|
||||
.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
match (part_number, parse_range_header(req, last_v_meta.size)?) {
|
||||
(Some(_), Some(_)) => Err(Error::bad_request(
|
||||
"Cannot specify both partNumber and Range header",
|
||||
|
|
|
@ -181,6 +181,16 @@ async fn test_website_s3_api() {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
ctx.client
|
||||
.put_object()
|
||||
.bucket(&bucket)
|
||||
.key("site/home-redirected.html")
|
||||
.website_redirect_location("/site/home.html")
|
||||
.body(ByteStream::from_static(&[]))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let conf = WebsiteConfiguration::builder()
|
||||
.index_document(
|
||||
IndexDocument::builder()
|
||||
|
@ -274,6 +284,29 @@ async fn test_website_s3_api() {
|
|||
);
|
||||
}
|
||||
|
||||
// Test redirection with CORS
|
||||
{
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("http://127.0.0.1:{}/site/home-redirected.html", ctx.garage.web_port))
|
||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||
.header("Origin", "https://example.com")
|
||||
.body(Body::new(Bytes::new()))
|
||||
.unwrap();
|
||||
|
||||
let resp = client.request(req).await.unwrap();
|
||||
|
||||
assert!(resp.status().is_redirection());
|
||||
assert_eq!(
|
||||
resp.headers().get("access-control-allow-origin").unwrap(),
|
||||
"*"
|
||||
);
|
||||
assert_eq!(
|
||||
resp.headers().get(http::header::LOCATION).unwrap(),
|
||||
"/site/home.html"
|
||||
);
|
||||
}
|
||||
|
||||
// Test ErrorDocument on 404
|
||||
{
|
||||
let req = Request::builder()
|
||||
|
|
|
@ -249,6 +249,9 @@ impl WebServer {
|
|||
handle_head_without_ctx(self.garage.clone(), req, bucket_id, &key, None).await
|
||||
}
|
||||
Method::GET => {
|
||||
// Redirects are not flattened in the web API, as per:
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-page-redirect.html
|
||||
// > Region-specific website endpoint – Amazon S3 redirects the page request according to the value of the x-amz-website-redirect-location property.
|
||||
handle_get_without_ctx(
|
||||
self.garage.clone(),
|
||||
req,
|
||||
|
@ -256,6 +259,7 @@ impl WebServer {
|
|||
&key,
|
||||
None,
|
||||
Default::default(),
|
||||
false
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
@ -309,6 +313,7 @@ impl WebServer {
|
|||
&error_document,
|
||||
None,
|
||||
Default::default(),
|
||||
false
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue