diff --git a/Cargo.lock b/Cargo.lock
index 84ab24ae..f278a361 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -675,6 +675,7 @@ dependencies = [
"git-version",
"hex",
"http",
+ "hyper",
"kuska-sodiumoxide",
"log",
"netapp",
diff --git a/Cargo.nix b/Cargo.nix
index 8855e196..48f23a85 100644
--- a/Cargo.nix
+++ b/Cargo.nix
@@ -560,7 +560,7 @@ in
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo { inherit name version; sha256 = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"; };
dependencies = {
- ${ if hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.115" { inherit profileName; };
+ ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.115" { inherit profileName; };
};
});
@@ -999,6 +999,7 @@ in
devDependencies = {
aws_sdk_s3 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-s3."0.6.0" { inherit profileName; };
http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.5" { inherit profileName; };
+ hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.13" { inherit profileName; };
static_init = rustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init."1.0.2" { inherit profileName; };
};
});
@@ -2704,7 +2705,7 @@ in
];
dependencies = {
bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; };
- ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.115" { inherit profileName; };
+ ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.115" { inherit profileName; };
${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; };
${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; };
static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; };
diff --git a/script/test-smoke.sh b/script/test-smoke.sh
index dc9b2d2d..53415775 100755
--- a/script/test-smoke.sh
+++ b/script/test-smoke.sh
@@ -141,28 +141,6 @@ rm eprouvette/winscp
EOF
fi
-# Advanced testing via S3API
-if [ -z "$SKIP_AWS" ]; then
- echo "Test CORS endpoints"
- garage -c /tmp/config.1.toml bucket website --allow eprouvette
- aws s3api put-object --bucket eprouvette --key index.html
- CORS='{"CORSRules":[{"AllowedHeaders":["*"],"AllowedMethods":["GET","PUT"],"AllowedOrigins":["*"]}]}'
- aws s3api put-bucket-cors --bucket eprouvette --cors-configuration $CORS
- [ `aws s3api get-bucket-cors --bucket eprouvette | jq -c` == $CORS ]
-
- curl -s -i -H 'Origin: http://example.com' --header "Host: eprouvette.web.garage.localhost" http://127.0.0.1:3921/ | grep access-control-allow-origin
- curl -s -i -X OPTIONS -H 'Access-Control-Request-Method: PUT' -H 'Origin: http://example.com' --header "Host: eprouvette.web.garage.localhost" http://127.0.0.1:3921/ | grep access-control-allow-methods
- curl -s -i -X OPTIONS -H 'Access-Control-Request-Method: DELETE' -H 'Origin: http://example.com' --header "Host: eprouvette.web.garage.localhost" http://127.0.0.1:3921/ | grep '403 Forbidden'
-
- #@TODO we may want to test the S3 endpoint but we need to handle authentication, which is way more complex.
-
- aws s3api delete-bucket-cors --bucket eprouvette
- ! [ -s `aws s3api get-bucket-cors --bucket eprouvette` ]
- curl -s -i -X OPTIONS -H 'Access-Control-Request-Method: PUT' -H 'Origin: http://example.com' --header "Host: eprouvette.web.garage.localhost" http://127.0.0.1:3921/ | grep '403 Forbidden'
- aws s3api delete-object --bucket eprouvette --key index.html
- garage -c /tmp/config.1.toml bucket website --deny eprouvette
-fi
-
rm /tmp/garage.{1..3}.{rnd,b64}
if [ -z "$SKIP_AWS" ]; then
@@ -180,19 +158,6 @@ if [ -z "$SKIP_AWS" ]; then
[ $(aws s3 ls | wc -l) == 1 ]
fi
-if [ -z "$SKIP_AWS" ]; then
- echo "๐งช Website Testing"
- echo "
hello world
" > /tmp/garage-index.html
- aws s3 cp /tmp/garage-index.html s3://eprouvette/index.html
- [ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.web.garage.localhost" http://127.0.0.1:3921/ ` == 404 ]
- garage -c /tmp/config.1.toml bucket website --allow eprouvette
- [ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.web.garage.localhost" http://127.0.0.1:3921/ ` == 200 ]
- garage -c /tmp/config.1.toml bucket website --deny eprouvette
- [ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.web.garage.localhost" http://127.0.0.1:3921/ ` == 404 ]
- aws s3 rm s3://eprouvette/index.html
- rm /tmp/garage-index.html
-fi
-
echo "๐ Teardown"
AWS_ACCESS_KEY_ID=`cat /tmp/garage.s3 |cut -d' ' -f1`
AWS_SECRET_ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml
index d6034bbd..463f83e7 100644
--- a/src/garage/Cargo.toml
+++ b/src/garage/Cargo.toml
@@ -55,5 +55,6 @@ netapp = "0.3.0"
[dev-dependencies]
aws-sdk-s3 = "0.6"
http = "0.2"
+hyper = { version = "0.14", features = ["client", "http1", "runtime"] }
static_init = "1.0"
diff --git a/src/garage/tests/common/garage.rs b/src/garage/tests/common/garage.rs
index b887856a..92aa2edf 100644
--- a/src/garage/tests/common/garage.rs
+++ b/src/garage/tests/common/garage.rs
@@ -6,7 +6,7 @@ use std::sync::Once;
use super::ext::*;
// https://xkcd.com/221/
-const DEFAULT_PORT: u16 = 49995;
+pub const DEFAULT_PORT: u16 = 49995;
static GARAGE_TEST_SECRET: &str =
"c3ea8cb80333d04e208d136698b1a01ae370d463f0d435ab2177510b3478bf44";
diff --git a/src/garage/tests/website.rs b/src/garage/tests/website.rs
index 8b137891..34093a79 100644
--- a/src/garage/tests/website.rs
+++ b/src/garage/tests/website.rs
@@ -1 +1,342 @@
+use crate::common;
+use crate::common::ext::*;
+use aws_sdk_s3::{
+ model::{CorsConfiguration, CorsRule, ErrorDocument, IndexDocument, WebsiteConfiguration},
+ ByteStream,
+};
+use http::Request;
+use hyper::{
+ body::{to_bytes, Body},
+ Client,
+};
+const BODY: &[u8; 16] = b"bonjour
";
+const BODY_ERR: &[u8; 6] = b"erreur";
+
+#[tokio::test]
+async fn test_website() {
+ const BCKT_NAME: &str = "my-website";
+ let ctx = common::context();
+ let bucket = ctx.create_bucket(BCKT_NAME);
+
+ let data = ByteStream::from_static(BODY);
+
+ ctx.client
+ .put_object()
+ .bucket(&bucket)
+ .key("index.html")
+ .body(data)
+ .send()
+ .await
+ .unwrap();
+
+ let client = Client::new();
+
+ let req = || {
+ Request::builder()
+ .method("GET")
+ .uri(format!(
+ "http://127.0.0.1:{}/",
+ common::garage::DEFAULT_PORT + 2
+ ))
+ .header("Host", format!("{}.web.garage", BCKT_NAME))
+ .body(Body::empty())
+ .unwrap()
+ };
+
+ let mut resp = client.request(req()).await.unwrap();
+
+ assert_eq!(resp.status(), 404);
+ assert_ne!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY.as_ref()
+ ); /* check that we do not leak body */
+
+ ctx.garage
+ .command()
+ .args(["bucket", "website", "--allow", BCKT_NAME])
+ .quiet()
+ .expect_success_status("Could not allow website on bucket");
+
+ resp = client.request(req()).await.unwrap();
+ assert_eq!(resp.status(), 200);
+ assert_eq!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY.as_ref()
+ );
+
+ ctx.garage
+ .command()
+ .args(["bucket", "website", "--deny", BCKT_NAME])
+ .quiet()
+ .expect_success_status("Could not deny website on bucket");
+
+ resp = client.request(req()).await.unwrap();
+ assert_eq!(resp.status(), 404);
+ assert_ne!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY.as_ref()
+ ); /* check that we do not leak body */
+}
+
+#[tokio::test]
+async fn test_website_s3_api() {
+ const BCKT_NAME: &str = "my-cors";
+ let ctx = common::context();
+ let bucket = ctx.create_bucket(BCKT_NAME);
+
+ let data = ByteStream::from_static(BODY);
+
+ ctx.client
+ .put_object()
+ .bucket(&bucket)
+ .key("site/home.html")
+ .body(data)
+ .send()
+ .await
+ .unwrap();
+
+ ctx.client
+ .put_object()
+ .bucket(&bucket)
+ .key("err/error.html")
+ .body(ByteStream::from_static(BODY_ERR))
+ .send()
+ .await
+ .unwrap();
+
+ let conf = WebsiteConfiguration::builder()
+ .index_document(IndexDocument::builder().suffix("home.html").build())
+ .error_document(ErrorDocument::builder().key("err/error.html").build())
+ .build();
+
+ ctx.client
+ .put_bucket_website()
+ .bucket(&bucket)
+ .website_configuration(conf)
+ .send()
+ .await
+ .unwrap();
+
+ let cors = CorsConfiguration::builder()
+ .cors_rules(
+ CorsRule::builder()
+ .id("main-rule")
+ .allowed_headers("*")
+ .allowed_methods("GET")
+ .allowed_methods("PUT")
+ .allowed_origins("*")
+ .build(),
+ )
+ .build();
+
+ ctx.client
+ .put_bucket_cors()
+ .bucket(&bucket)
+ .cors_configuration(cors)
+ .send()
+ .await
+ .unwrap();
+
+ {
+ let cors_res = ctx
+ .client
+ .get_bucket_cors()
+ .bucket(&bucket)
+ .send()
+ .await
+ .unwrap();
+
+ let main_rule = cors_res.cors_rules().unwrap().iter().next().unwrap();
+
+ assert_eq!(main_rule.id.as_ref().unwrap(), "main-rule");
+ assert_eq!(
+ main_rule.allowed_headers.as_ref().unwrap(),
+ &vec!["*".to_string()]
+ );
+ assert_eq!(
+ main_rule.allowed_origins.as_ref().unwrap(),
+ &vec!["*".to_string()]
+ );
+ assert_eq!(
+ main_rule.allowed_methods.as_ref().unwrap(),
+ &vec!["GET".to_string(), "PUT".to_string()]
+ );
+ }
+
+ let client = Client::new();
+
+ // Test direct requests with CORS
+ {
+ let req = Request::builder()
+ .method("GET")
+ .uri(format!(
+ "http://127.0.0.1:{}/site/",
+ common::garage::DEFAULT_PORT + 2
+ ))
+ .header("Host", format!("{}.web.garage", BCKT_NAME))
+ .header("Origin", "https://example.com")
+ .body(Body::empty())
+ .unwrap();
+
+ let mut resp = client.request(req).await.unwrap();
+
+ assert_eq!(resp.status(), 200);
+ assert_eq!(
+ resp.headers().get("access-control-allow-origin").unwrap(),
+ "*"
+ );
+ assert_eq!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY.as_ref()
+ );
+ }
+
+ // Test ErrorDocument on 404
+ {
+ let req = Request::builder()
+ .method("GET")
+ .uri(format!(
+ "http://127.0.0.1:{}/wrong.html",
+ common::garage::DEFAULT_PORT + 2
+ ))
+ .header("Host", format!("{}.web.garage", BCKT_NAME))
+ .body(Body::empty())
+ .unwrap();
+
+ let mut resp = client.request(req).await.unwrap();
+
+ assert_eq!(resp.status(), 404);
+ assert_eq!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY_ERR.as_ref()
+ );
+ }
+
+ // Test CORS with an allowed preflight request
+ {
+ let req = Request::builder()
+ .method("OPTIONS")
+ .uri(format!(
+ "http://127.0.0.1:{}/site/",
+ common::garage::DEFAULT_PORT + 2
+ ))
+ .header("Host", format!("{}.web.garage", BCKT_NAME))
+ .header("Origin", "https://example.com")
+ .header("Access-Control-Request-Method", "PUT")
+ .body(Body::empty())
+ .unwrap();
+
+ let mut resp = client.request(req).await.unwrap();
+
+ assert_eq!(resp.status(), 200);
+ assert_eq!(
+ resp.headers().get("access-control-allow-origin").unwrap(),
+ "*"
+ );
+ assert_ne!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY.as_ref()
+ );
+ }
+
+ // Test CORS with a forbidden preflight request
+ {
+ let req = Request::builder()
+ .method("OPTIONS")
+ .uri(format!(
+ "http://127.0.0.1:{}/site/",
+ common::garage::DEFAULT_PORT + 2
+ ))
+ .header("Host", format!("{}.web.garage", BCKT_NAME))
+ .header("Origin", "https://example.com")
+ .header("Access-Control-Request-Method", "DELETE")
+ .body(Body::empty())
+ .unwrap();
+
+ let mut resp = client.request(req).await.unwrap();
+
+ assert_eq!(resp.status(), 403);
+ assert_ne!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY.as_ref()
+ );
+ }
+
+ //@TODO test CORS on the S3 endpoint. We need to handle auth manually to check it.
+
+ // Delete cors
+ ctx.client
+ .delete_bucket_cors()
+ .bucket(&bucket)
+ .send()
+ .await
+ .unwrap();
+
+ // Check CORS are deleted from the API
+ // @FIXME check what is the expected behavior when GetBucketCors is called on a bucket without
+ // any CORS.
+ assert!(ctx
+ .client
+ .get_bucket_cors()
+ .bucket(&bucket)
+ .send()
+ .await
+ .is_err());
+
+ // Test CORS are not sent anymore on a previously allowed request
+ {
+ let req = Request::builder()
+ .method("OPTIONS")
+ .uri(format!(
+ "http://127.0.0.1:{}/site/",
+ common::garage::DEFAULT_PORT + 2
+ ))
+ .header("Host", format!("{}.web.garage", BCKT_NAME))
+ .header("Origin", "https://example.com")
+ .header("Access-Control-Request-Method", "PUT")
+ .body(Body::empty())
+ .unwrap();
+
+ let mut resp = client.request(req).await.unwrap();
+
+ assert_eq!(resp.status(), 403);
+ assert_ne!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY.as_ref()
+ );
+ }
+
+ // Disallow website from the API
+ ctx.client
+ .delete_bucket_website()
+ .bucket(&bucket)
+ .send()
+ .await
+ .unwrap();
+
+ // Check that the website is not served anymore
+ {
+ let req = Request::builder()
+ .method("GET")
+ .uri(format!(
+ "http://127.0.0.1:{}/site/",
+ common::garage::DEFAULT_PORT + 2
+ ))
+ .header("Host", format!("{}.web.garage", BCKT_NAME))
+ .body(Body::empty())
+ .unwrap();
+
+ let mut resp = client.request(req).await.unwrap();
+
+ assert_eq!(resp.status(), 404);
+ assert_ne!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY_ERR.as_ref()
+ );
+ assert_ne!(
+ to_bytes(resp.body_mut()).await.unwrap().as_ref(),
+ BODY.as_ref()
+ );
+ }
+}