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",
|
"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]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
|
@ -1365,6 +1376,7 @@ dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
|
"async-recursion",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
15
Cargo.nix
15
Cargo.nix
|
@ -34,7 +34,7 @@ args@{
|
||||||
ignoreLockHash,
|
ignoreLockHash,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
nixifiedLockHash = "c0aa85d369b22875a652356862a5810c22838970be9fbec558dd108d5232881d";
|
nixifiedLockHash = "fe0e77c1af963cc04accebb4ddd1607df5bbbaa48ca0b7a325f5e4497e21790e";
|
||||||
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
|
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
|
||||||
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
|
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
|
||||||
lockHashIgnored = if ignoreLockHash
|
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 {
|
"registry+https://github.com/rust-lang/crates.io-index".async-stream."0.3.5" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "async-stream";
|
name = "async-stream";
|
||||||
version = "0.3.5";
|
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;
|
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;
|
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_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;
|
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;
|
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;
|
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.workspace = true
|
||||||
opentelemetry-prometheus = { workspace = true, optional = true }
|
opentelemetry-prometheus = { workspace = true, optional = true }
|
||||||
prometheus = { workspace = true, optional = true }
|
prometheus = { workspace = true, optional = true }
|
||||||
|
async-recursion = "1.1.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
|
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
|
||||||
|
|
|
@ -201,7 +201,10 @@ impl ApiHandler for S3ApiServer {
|
||||||
response_content_type,
|
response_content_type,
|
||||||
response_expires,
|
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 {
|
Endpoint::UploadPart {
|
||||||
key,
|
key,
|
||||||
|
|
|
@ -280,22 +280,25 @@ pub async fn handle_head_without_ctx(
|
||||||
/// Handle GET request
|
/// Handle GET request
|
||||||
pub async fn handle_get(
|
pub async fn handle_get(
|
||||||
ctx: ReqCtx,
|
ctx: ReqCtx,
|
||||||
req: &Request<impl Body>,
|
req: &Request<impl Body + std::marker::Sync>,
|
||||||
key: &str,
|
key: &str,
|
||||||
part_number: Option<u64>,
|
part_number: Option<u64>,
|
||||||
overrides: GetObjectOverrides,
|
overrides: GetObjectOverrides,
|
||||||
|
flatten_redirect: bool
|
||||||
) -> Result<Response<ResBody>, Error> {
|
) -> 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
|
/// Handle GET request
|
||||||
|
#[async_recursion::async_recursion]
|
||||||
pub async fn handle_get_without_ctx(
|
pub async fn handle_get_without_ctx(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: &Request<impl Body>,
|
req: &Request<impl Body + std::marker::Sync>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
key: &str,
|
key: &str,
|
||||||
part_number: Option<u64>,
|
part_number: Option<u64>,
|
||||||
overrides: GetObjectOverrides,
|
overrides: GetObjectOverrides,
|
||||||
|
flatten_redirect: bool
|
||||||
) -> Result<Response<ResBody>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let object = garage
|
let object = garage
|
||||||
.object_table
|
.object_table
|
||||||
|
@ -329,6 +332,30 @@ pub async fn handle_get_without_ctx(
|
||||||
|
|
||||||
let checksum_mode = checksum_mode(&req);
|
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)?) {
|
match (part_number, parse_range_header(req, last_v_meta.size)?) {
|
||||||
(Some(_), Some(_)) => Err(Error::bad_request(
|
(Some(_), Some(_)) => Err(Error::bad_request(
|
||||||
"Cannot specify both partNumber and Range header",
|
"Cannot specify both partNumber and Range header",
|
||||||
|
|
|
@ -181,6 +181,16 @@ async fn test_website_s3_api() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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()
|
let conf = WebsiteConfiguration::builder()
|
||||||
.index_document(
|
.index_document(
|
||||||
IndexDocument::builder()
|
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
|
// Test ErrorDocument on 404
|
||||||
{
|
{
|
||||||
let req = Request::builder()
|
let req = Request::builder()
|
||||||
|
|
|
@ -249,6 +249,9 @@ impl WebServer {
|
||||||
handle_head_without_ctx(self.garage.clone(), req, bucket_id, &key, None).await
|
handle_head_without_ctx(self.garage.clone(), req, bucket_id, &key, None).await
|
||||||
}
|
}
|
||||||
Method::GET => {
|
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(
|
handle_get_without_ctx(
|
||||||
self.garage.clone(),
|
self.garage.clone(),
|
||||||
req,
|
req,
|
||||||
|
@ -256,6 +259,7 @@ impl WebServer {
|
||||||
&key,
|
&key,
|
||||||
None,
|
None,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
|
false
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -309,6 +313,7 @@ impl WebServer {
|
||||||
&error_document,
|
&error_document,
|
||||||
None,
|
None,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
|
false
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue