Compare commits

...

32 Commits

Author SHA1 Message Date
Alex Auvolat 12f15c4c2b fix readme paths in cargo.toml for new crates 2025-03-05 11:00:19 +01:00
Alex Auvolat 42c5d02cdf doc: fix "since vX.X.X" in multiple places 2025-03-05 10:19:51 +01:00
Alex Auvolat 4689b10448 bump version to v1.1.0 2025-03-05 10:19:51 +01:00
Alex 156b10ee65 Merge pull request 'admin api definition: fix globalAlias query parameter name (related: #971)' (#973) from admin-sdk-fix into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/973
2025-03-05 09:19:30 +00:00
Alex Auvolat 8647ebf003 admin api definition: fix globalAlias query parameter name (related: #971) 2025-03-05 10:16:36 +01:00
maximilien 67d7c0769b Merge pull request 'Add headless service for statefulSet serviceName' (#970) from babykart/garage:helm-headless-svc into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/970
Reviewed-by: maximilien <me@mricher.fr>
2025-03-05 08:59:36 +00:00
babykart 09ed5ab8cc Fix documentation link
Signed-off-by: babykart <babykart@gmail.com>
2025-02-23 15:55:01 +01:00
babykart a0ea28b0da Add headless service for statefulSet serviceName
Signed-off-by: babykart <babykart@gmail.com>
2025-02-23 15:45:55 +01:00
Alex c5237c31e7 Merge pull request 'Implement all HTTP preconditions in GetObject/HeadObject' (#967) from fix-804 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/967
2025-02-19 17:31:26 +00:00
Alex Auvolat f87943a39d tests: add test for http preconditions 2025-02-19 18:26:03 +01:00
Alex Auvolat c0846c56fe api: unify http precondition handling 2025-02-19 18:14:27 +01:00
Alex 1cb0ae10a8 Merge pull request 'fix crash in layout computation when changing all nodes of a zone to gateway mode' (#937) from baptiste/garage:fix_layout_crash into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/937
Reviewed-by: Alex <lx@deuxfleurs.fr>
2025-02-19 17:09:10 +00:00
Alex Auvolat 1a8f74fc94 api: GetObject: implement if-match and if-unmodified-since 2025-02-19 17:26:29 +01:00
Alex 2191620af5 Merge pull request 'web: implement x-amz-website-redirect-location' (#966) from redirect-location-header into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/966
2025-02-19 16:10:04 +00:00
Alex Auvolat bf27a3ec98 web: implement x-amz-website-redirect-location 2025-02-19 17:04:10 +01:00
Alex f64ec6e542 Merge pull request 'implement STREAMING-*-PAYLOAD-TRAILER' (#960) from fix-824 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/960
2025-02-19 09:59:32 +00:00
Alex Auvolat 6d38907dac test: verify saved checksums in streaming putobject tests 2025-02-18 22:02:04 +01:00
Alex Auvolat cfe8e8d45c api: PutObject: save trailer checksum in metadata 2025-02-18 21:56:32 +01:00
Alex Auvolat f6e805e7db api: various fixes 2025-02-18 21:47:53 +01:00
Alex Auvolat 45e10e55f9 update aws-sdk-s3 in tests and fix wrong checksumming behavior in GetObject 2025-02-18 15:33:42 +01:00
Alex Auvolat 730bfee753 api: validate trailing checksum + add test for unsigned-paylad-trailer 2025-02-18 15:33:42 +01:00
Alex Auvolat ccab0e4ae5 api: fix optional \n after trailer checksum header 2025-02-18 15:33:42 +01:00
Alex Auvolat abb60dcf7e api: remove content-encoding: aws-chunked for streaming payload 2025-02-18 15:33:42 +01:00
Alex Auvolat f8b0817ddc api: streaming signature: fix trailer parsing 2025-02-18 12:00:41 +01:00
Alex Auvolat 21c0dda16a api: refactor: move checksumming code around again 2025-02-17 20:11:06 +01:00
Alex Auvolat 658541d812 api: use checksumming in api_common::signature for put/putpart 2025-02-17 19:54:25 +01:00
Alex Auvolat c5df820e2c api: start refactor of signature to calculate checksums earlier 2025-02-17 18:47:06 +01:00
Alex Auvolat a04d6cd5b8 api: streaming: parse unsigned streaming bodies and payload trailers 2025-02-17 16:23:24 +01:00
Alex Auvolat 44a896f9b5 api: add logic to parse x-amz-content-sha256 2025-02-16 18:25:35 +01:00
Alex Auvolat cee7560fc1 api: refactor: move checksum algorithms to common 2025-02-16 17:25:55 +01:00
Alex Auvolat 2f0c5ca220 signature: refactor: move constant defs to mod.rs 2025-02-16 16:34:18 +01:00
Baptiste Jonglez 6d798c640f WIP: fix crash in layout computation when changing all nodes of a zone to gateway mode
This change is probably not a proper fix, somebody with more expertise on
this code should look at it.

Here is how to reproduce the crash:

- start with a layout with two zones
- move all nodes of a zone to gateway mode: `garage layout assign fea54bcc081f318 -g`
- `garage layout show` will panic with a backtrace

Fortunately, the crash is only on the RPC client side, not on the Garage
server itself, and `garage layout revert` still works to go back to the
previous state.

As far as I can tell, this bug is present since Garage 0.9.0 which
includes the new layout assignation algorithm:

  https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/296
2025-01-27 19:33:57 +01:00
57 changed files with 1794 additions and 789 deletions
Generated
+84 -45
View File
@@ -238,9 +238,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "aws-credential-types" name = "aws-credential-types"
version = "1.1.4" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cc49dcdd31c8b6e79850a179af4c367669150c7ac0135f176c61bec81a70f7" checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da"
dependencies = [ dependencies = [
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
@@ -250,15 +250,16 @@ dependencies = [
[[package]] [[package]]
name = "aws-runtime" name = "aws-runtime"
version = "1.1.4" version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb031bff99877c26c28895766f7bb8484a05e24547e370768d6cc9db514662aa" checksum = "76dd04d39cc12844c0994f2c9c5a6f5184c22e9188ec1ff723de41910a21dcad"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-sigv4", "aws-sigv4",
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-eventstream", "aws-smithy-eventstream",
"aws-smithy-http", "aws-smithy-http",
"aws-smithy-runtime",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-types", "aws-smithy-types",
"aws-types", "aws-types",
@@ -266,6 +267,7 @@ dependencies = [
"fastrand", "fastrand",
"http 0.2.12", "http 0.2.12",
"http-body 0.4.6", "http-body 0.4.6",
"once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"tracing", "tracing",
@@ -274,9 +276,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-sdk-config" name = "aws-sdk-config"
version = "1.13.0" version = "1.62.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4af4f5b0f64563ada272e009cc95027effb546110ed85d014611420ac0d97858" checksum = "0f94d79b8eef608af51b5415d13f5c670dec177880c6f78cd27bea968e5c9b76"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-runtime", "aws-runtime",
@@ -296,9 +298,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-sdk-s3" name = "aws-sdk-s3"
version = "1.14.0" version = "1.68.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "951f7730f51a2155c711c85c79f337fbc02a577fa99d2a0a8059acfce5392113" checksum = "bc5ddf1dc70287dc9a2f953766a1fe15e3e74aef02fd1335f2afa475c9b4f4fc"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-runtime", "aws-runtime",
@@ -314,20 +316,25 @@ dependencies = [
"aws-smithy-xml", "aws-smithy-xml",
"aws-types", "aws-types",
"bytes", "bytes",
"fastrand",
"hex",
"hmac",
"http 0.2.12", "http 0.2.12",
"http-body 0.4.6", "http-body 0.4.6",
"lru",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"regex-lite", "regex-lite",
"sha2",
"tracing", "tracing",
"url", "url",
] ]
[[package]] [[package]]
name = "aws-sigv4" name = "aws-sigv4"
version = "1.1.4" version = "1.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c371c6b0ac54d4605eb6f016624fb5c7c2925d315fdf600ac1bf21b19d5f1742" checksum = "9bfe75fad52793ce6dec0dc3d4b1f388f038b5eb866c8d4d7f3a8e21b5ea5051"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-smithy-eventstream", "aws-smithy-eventstream",
@@ -354,9 +361,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-smithy-async" name = "aws-smithy-async"
version = "1.1.4" version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ee2d09cce0ef3ae526679b522835d63e75fb427aca5413cd371e490d52dcc6" checksum = "fa59d1327d8b5053c54bf2eaae63bf629ba9e904434d0835a28ed3c0ed0a614e"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"pin-project-lite", "pin-project-lite",
@@ -365,9 +372,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-smithy-checksums" name = "aws-smithy-checksums"
version = "0.60.4" version = "0.60.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be2acd1b9c6ae5859999250ed5a62423aedc5cf69045b844432de15fa2f31f2b" checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795"
dependencies = [ dependencies = [
"aws-smithy-http", "aws-smithy-http",
"aws-smithy-types", "aws-smithy-types",
@@ -386,9 +393,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-smithy-eventstream" name = "aws-smithy-eventstream"
version = "0.60.4" version = "0.60.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858" checksum = "8b18559a41e0c909b77625adf2b8c50de480a8041e5e4a3f5f7d177db70abc5a"
dependencies = [ dependencies = [
"aws-smithy-types", "aws-smithy-types",
"bytes", "bytes",
@@ -397,9 +404,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-smithy-http" name = "aws-smithy-http"
version = "0.60.4" version = "0.60.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab56aea3cd9e1101a0a999447fb346afb680ab1406cebc44b32346e25b4117d" checksum = "7809c27ad8da6a6a68c454e651d4962479e81472aa19ae99e59f9aba1f9713cc"
dependencies = [ dependencies = [
"aws-smithy-eventstream", "aws-smithy-eventstream",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
@@ -418,18 +425,18 @@ dependencies = [
[[package]] [[package]]
name = "aws-smithy-json" name = "aws-smithy-json"
version = "0.60.4" version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3898ca6518f9215f62678870064398f00031912390efd03f1f6ef56d83aa8e" checksum = "623a51127f24c30776c8b374295f2df78d92517386f77ba30773f15a30ce1422"
dependencies = [ dependencies = [
"aws-smithy-types", "aws-smithy-types",
] ]
[[package]] [[package]]
name = "aws-smithy-runtime" name = "aws-smithy-runtime"
version = "1.1.4" version = "1.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fafdab38f40ad7816e7da5dec279400dd505160780083759f01441af1bbb10ea" checksum = "d526a12d9ed61fadefda24abe2e682892ba288c2018bcb38b1b4c111d13f6d92"
dependencies = [ dependencies = [
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-http", "aws-smithy-http",
@@ -440,6 +447,8 @@ dependencies = [
"h2 0.3.24", "h2 0.3.24",
"http 0.2.12", "http 0.2.12",
"http-body 0.4.6", "http-body 0.4.6",
"http-body 1.0.1",
"httparse",
"hyper 0.14.32", "hyper 0.14.32",
"hyper-rustls 0.24.2", "hyper-rustls 0.24.2",
"once_cell", "once_cell",
@@ -452,31 +461,36 @@ dependencies = [
[[package]] [[package]]
name = "aws-smithy-runtime-api" name = "aws-smithy-runtime-api"
version = "1.1.4" version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c18276dd28852f34b3bf501f4f3719781f4999a51c7bff1a5c6dc8c4529adc29" checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd"
dependencies = [ dependencies = [
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-types", "aws-smithy-types",
"bytes", "bytes",
"http 0.2.12", "http 0.2.12",
"http 1.2.0",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tracing", "tracing",
"zeroize",
] ]
[[package]] [[package]]
name = "aws-smithy-types" name = "aws-smithy-types"
version = "1.1.4" version = "1.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb3e134004170d3303718baa2a4eb4ca64ee0a1c0a7041dca31b38be0fb414f3" checksum = "c7b8a53819e42f10d0821f56da995e1470b199686a1809168db6ca485665f042"
dependencies = [ dependencies = [
"base64-simd", "base64-simd",
"bytes", "bytes",
"bytes-utils", "bytes-utils",
"futures-core", "futures-core",
"http 0.2.12", "http 0.2.12",
"http 1.2.0",
"http-body 0.4.6", "http-body 0.4.6",
"http-body 1.0.1",
"http-body-util",
"itoa", "itoa",
"num-integer", "num-integer",
"pin-project-lite", "pin-project-lite",
@@ -490,24 +504,23 @@ dependencies = [
[[package]] [[package]]
name = "aws-smithy-xml" name = "aws-smithy-xml"
version = "0.60.4" version = "0.60.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8604a11b25e9ecaf32f9aa56b9fe253c5e2f606a3477f0071e96d3155a5ed218" checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc"
dependencies = [ dependencies = [
"xmlparser", "xmlparser",
] ]
[[package]] [[package]]
name = "aws-types" name = "aws-types"
version = "1.1.4" version = "1.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "789bbe008e65636fe1b6dbbb374c40c8960d1232b96af5ff4aec349f9c4accf4" checksum = "dfbd0a668309ec1f66c0f6bda4840dd6d4796ae26d699ebc266d7cc95c6d040f"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-types", "aws-smithy-types",
"http 0.2.12",
"rustc_version", "rustc_version",
"tracing", "tracing",
] ]
@@ -1116,6 +1129,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@@ -1220,7 +1239,7 @@ dependencies = [
[[package]] [[package]]
name = "garage" name = "garage"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"assert-json-diff", "assert-json-diff",
"async-trait", "async-trait",
@@ -1230,6 +1249,7 @@ dependencies = [
"bytes", "bytes",
"bytesize", "bytesize",
"chrono", "chrono",
"crc32fast",
"format_table", "format_table",
"futures", "futures",
"garage_api_admin", "garage_api_admin",
@@ -1273,7 +1293,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_api_admin" name = "garage_api_admin"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"argon2", "argon2",
"async-trait", "async-trait",
@@ -1299,10 +1319,13 @@ dependencies = [
[[package]] [[package]]
name = "garage_api_common" name = "garage_api_common"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"base64 0.21.7",
"bytes", "bytes",
"chrono", "chrono",
"crc32c",
"crc32fast",
"crypto-common", "crypto-common",
"err-derive", "err-derive",
"futures", "futures",
@@ -1316,11 +1339,13 @@ dependencies = [
"hyper 1.6.0", "hyper 1.6.0",
"hyper-util", "hyper-util",
"idna 0.5.0", "idna 0.5.0",
"md-5",
"nom", "nom",
"opentelemetry", "opentelemetry",
"pin-project", "pin-project",
"serde", "serde",
"serde_json", "serde_json",
"sha1",
"sha2", "sha2",
"tokio", "tokio",
"tracing", "tracing",
@@ -1329,7 +1354,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_api_k2v" name = "garage_api_k2v"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"err-derive", "err-derive",
@@ -1352,7 +1377,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_api_s3" name = "garage_api_s3"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"async-compression", "async-compression",
@@ -1397,7 +1422,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_block" name = "garage_block"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-compression", "async-compression",
@@ -1422,7 +1447,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_db" name = "garage_db"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"err-derive", "err-derive",
"heed", "heed",
@@ -1435,7 +1460,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_model" name = "garage_model"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.21.7", "base64 0.21.7",
@@ -1462,7 +1487,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_net" name = "garage_net"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"bytes", "bytes",
@@ -1487,7 +1512,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_rpc" name = "garage_rpc"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@@ -1519,7 +1544,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_table" name = "garage_table"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@@ -1540,7 +1565,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_util" name = "garage_util"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@@ -1572,7 +1597,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_web" name = "garage_web"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"err-derive", "err-derive",
"garage_api_common", "garage_api_common",
@@ -1738,6 +1763,11 @@ name = "hashbrown"
version = "0.15.2" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]] [[package]]
name = "hashlink" name = "hashlink"
@@ -2594,6 +2624,15 @@ version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.2",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@@ -4909,7 +4948,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
+14 -14
View File
@@ -24,18 +24,18 @@ default-members = ["src/garage"]
# Internal Garage crates # Internal Garage crates
format_table = { version = "0.1.1", path = "src/format-table" } format_table = { version = "0.1.1", path = "src/format-table" }
garage_api_common = { version = "1.0.1", path = "src/api/common" } garage_api_common = { version = "1.1.0", path = "src/api/common" }
garage_api_admin = { version = "1.0.1", path = "src/api/admin" } garage_api_admin = { version = "1.1.0", path = "src/api/admin" }
garage_api_s3 = { version = "1.0.1", path = "src/api/s3" } garage_api_s3 = { version = "1.1.0", path = "src/api/s3" }
garage_api_k2v = { version = "1.0.1", path = "src/api/k2v" } garage_api_k2v = { version = "1.1.0", path = "src/api/k2v" }
garage_block = { version = "1.0.1", path = "src/block" } garage_block = { version = "1.1.0", path = "src/block" }
garage_db = { version = "1.0.1", path = "src/db", default-features = false } garage_db = { version = "1.1.0", path = "src/db", default-features = false }
garage_model = { version = "1.0.1", path = "src/model", default-features = false } garage_model = { version = "1.1.0", path = "src/model", default-features = false }
garage_net = { version = "1.0.1", path = "src/net" } garage_net = { version = "1.1.0", path = "src/net" }
garage_rpc = { version = "1.0.1", path = "src/rpc" } garage_rpc = { version = "1.1.0", path = "src/rpc" }
garage_table = { version = "1.0.1", path = "src/table" } garage_table = { version = "1.1.0", path = "src/table" }
garage_util = { version = "1.0.1", path = "src/util" } garage_util = { version = "1.1.0", path = "src/util" }
garage_web = { version = "1.0.1", path = "src/web" } garage_web = { version = "1.1.0", path = "src/web" }
k2v-client = { version = "0.0.4", path = "src/k2v-client" } k2v-client = { version = "0.0.4", path = "src/k2v-client" }
# External crates from crates.io # External crates from crates.io
@@ -141,8 +141,8 @@ thiserror = "1.0"
assert-json-diff = "2.0" assert-json-diff = "2.0"
rustc_version = "0.4.0" rustc_version = "0.4.0"
static_init = "1.0" static_init = "1.0"
aws-sdk-config = "1.13" aws-sdk-config = "1.62"
aws-sdk-s3 = "1.14" aws-sdk-s3 = "=1.68"
[profile.dev] [profile.dev]
#lto = "thin" # disabled for now, adds 2-4 min to each CI build #lto = "thin" # disabled for now, adds 2-4 min to each CI build
+2 -2
View File
@@ -687,7 +687,7 @@ paths:
operationId: "GetBucketInfo" operationId: "GetBucketInfo"
summary: "Get a bucket" summary: "Get a bucket"
description: | description: |
Given a bucket identifier (`id`) or a global alias (`alias`), get its information. Given a bucket identifier (`id`) or a global alias (`globalAlias`), get its information.
It includes its aliases, its web configuration, keys that have some permissions It includes its aliases, its web configuration, keys that have some permissions
on it, some statistics (number of objects, size), number of dangling multipart uploads, on it, some statistics (number of objects, size), number of dangling multipart uploads,
and its quotas (if any). and its quotas (if any).
@@ -701,7 +701,7 @@ paths:
example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87" example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87"
schema: schema:
type: string type: string
- name: alias - name: globalAlias
in: query in: query
description: | description: |
The exact global alias of one of the existing buckets. The exact global alias of one of the existing buckets.
+5 -5
View File
@@ -96,14 +96,14 @@ to store 2 TB of data in total.
## Get a Docker image ## Get a Docker image
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated). Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
We encourage you to use a fixed tag (eg. `v1.0.1`) and not the `latest` tag. We encourage you to use a fixed tag (eg. `v1.1.0`) and not the `latest` tag.
For this example, we will use the latest published version at the time of the writing which is `v1.0.1` but it's up to you For this example, we will use the latest published version at the time of the writing which is `v1.1.0` but it's up to you
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated). to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
For example: For example:
``` ```
sudo docker pull dxflrs/garage:v1.0.1 sudo docker pull dxflrs/garage:v1.1.0
``` ```
## Deploying and configuring Garage ## Deploying and configuring Garage
@@ -171,7 +171,7 @@ docker run \
-v /etc/garage.toml:/etc/garage.toml \ -v /etc/garage.toml:/etc/garage.toml \
-v /var/lib/garage/meta:/var/lib/garage/meta \ -v /var/lib/garage/meta:/var/lib/garage/meta \
-v /var/lib/garage/data:/var/lib/garage/data \ -v /var/lib/garage/data:/var/lib/garage/data \
dxflrs/garage:v1.0.1 dxflrs/garage:v1.1.0
``` ```
With this command line, Garage should be started automatically at each boot. With this command line, Garage should be started automatically at each boot.
@@ -185,7 +185,7 @@ If you want to use `docker-compose`, you may use the following `docker-compose.y
version: "3" version: "3"
services: services:
garage: garage:
image: dxflrs/garage:v1.0.1 image: dxflrs/garage:v1.1.0
network_mode: "host" network_mode: "host"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+6 -6
View File
@@ -154,7 +154,7 @@ The `[admin]` section:
The following configuration parameter must be specified as an environment The following configuration parameter must be specified as an environment
variable, it does not exist in the configuration file: variable, it does not exist in the configuration file:
- `GARAGE_LOG_TO_SYSLOG` (since v0.9.4): set this to `1` or `true` to make the - `GARAGE_LOG_TO_SYSLOG` (since `v0.9.4`): set this to `1` or `true` to make the
Garage daemon send its logs to `syslog` (using the libc `syslog` function) Garage daemon send its logs to `syslog` (using the libc `syslog` function)
instead of printing to stderr. instead of printing to stderr.
@@ -300,7 +300,7 @@ data_dir = [
See [the dedicated documentation page](@/documentation/operations/multi-hdd.md) See [the dedicated documentation page](@/documentation/operations/multi-hdd.md)
on how to operate Garage in such a setup. on how to operate Garage in such a setup.
#### `metadata_snapshots_dir` (since Garage `v1.0.2`) {#metadata_snapshots_dir} #### `metadata_snapshots_dir` (since `v1.1.0`) {#metadata_snapshots_dir}
The directory in which Garage will store metadata snapshots when it The directory in which Garage will store metadata snapshots when it
performs a snapshot of the metadata database, either when instructed to do performs a snapshot of the metadata database, either when instructed to do
@@ -416,7 +416,7 @@ at the cost of a moderate drop in write performance.
Similarly to `metatada_fsync`, this is likely not necessary Similarly to `metatada_fsync`, this is likely not necessary
if geographical replication is used. if geographical replication is used.
#### `metadata_auto_snapshot_interval` (since Garage v0.9.4) {#metadata_auto_snapshot_interval} #### `metadata_auto_snapshot_interval` (since `v0.9.4`) {#metadata_auto_snapshot_interval}
If this value is set, Garage will automatically take a snapshot of the metadata If this value is set, Garage will automatically take a snapshot of the metadata
DB file at a regular interval and save it in the metadata directory. DB file at a regular interval and save it in the metadata directory.
@@ -453,7 +453,7 @@ you should delete it from the data directory and then call `garage repair
blocks` on the node to ensure that it re-obtains a copy from another node on blocks` on the node to ensure that it re-obtains a copy from another node on
the network. the network.
#### `use_local_tz` {#use_local_tz} #### `use_local_tz` (since `v1.1.0`) {#use_local_tz}
By default, Garage runs the lifecycle worker every day at midnight in UTC. Set the By default, Garage runs the lifecycle worker every day at midnight in UTC. Set the
`use_local_tz` configuration value to `true` if you want Garage to run the `use_local_tz` configuration value to `true` if you want Garage to run the
@@ -475,7 +475,7 @@ files will remain available. This however means that chunks from existing files
will not be deduplicated with chunks from newly uploaded files, meaning you will not be deduplicated with chunks from newly uploaded files, meaning you
might use more storage space that is optimally possible. might use more storage space that is optimally possible.
#### `block_ram_buffer_max` (since v0.9.4) {#block_ram_buffer_max} #### `block_ram_buffer_max` (since `v0.9.4`) {#block_ram_buffer_max}
A limit on the total size of data blocks kept in RAM by S3 API nodes awaiting A limit on the total size of data blocks kept in RAM by S3 API nodes awaiting
to be sent to storage nodes asynchronously. to be sent to storage nodes asynchronously.
@@ -562,7 +562,7 @@ the node, even in the case of a NAT: the NAT should be configured to forward the
port number to the same internal port nubmer. This means that if you have several nodes running port number to the same internal port nubmer. This means that if you have several nodes running
behind a NAT, they should each use a different RPC port number. behind a NAT, they should each use a different RPC port number.
#### `rpc_bind_outgoing`(since v0.9.2) {#rpc_bind_outgoing} #### `rpc_bind_outgoing` (since `v0.9.2`) {#rpc_bind_outgoing}
If enabled, pre-bind all sockets for outgoing connections to the same IP address If enabled, pre-bind all sockets for outgoing connections to the same IP address
used for listening (the IP address specified in `rpc_bind_addr`) before used for listening (the IP address specified in `rpc_bind_addr`) before
+1 -1
View File
@@ -70,7 +70,7 @@ Example response body:
```json ```json
{ {
"node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df", "node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df",
"garageVersion": "v1.0.1", "garageVersion": "v1.1.0",
"garageFeatures": [ "garageFeatures": [
"k2v", "k2v",
"lmdb", "lmdb",
+1 -1
View File
@@ -11,7 +11,7 @@ PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:${NIX_RELEASE}:$PATH"
FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m") FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m")
export RUST_BACKTRACE=1 export RUST_BACKTRACE=1
export RUST_LOG=garage=info,garage_api=debug export RUST_LOG=garage=info,garage_api_common=debug,garage_api_s3=debug
MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m" MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m"
if [ -z "$GARAGE_BIN" ]; then if [ -z "$GARAGE_BIN" ]; then
+1 -1
View File
@@ -1,3 +1,3 @@
# Garage helm3 chart # Garage helm3 chart
Documentation is located [here](/doc/book/cookbook/kubernetes.md). Documentation is located [here](https://garagehq.deuxfleurs.fr/documentation/cookbook/kubernetes/).
+2 -2
View File
@@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.6.0 version: 0.7.0
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "v1.0.1" appVersion: "v1.1.0"
@@ -0,0 +1,21 @@
{{- if eq .Values.deployment.kind "StatefulSet" -}}
apiVersion: v1
kind: Service
metadata:
name: {{ include "garage.fullname" . }}-headless
labels:
{{- include "garage.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.s3.api.port }}
targetPort: 3900
protocol: TCP
name: s3-api
- port: {{ .Values.service.s3.web.port }}
targetPort: 3902
protocol: TCP
name: s3-web
selector:
{{- include "garage.selectorLabels" . | nindent 4 }}
{{- end }}
+1 -2
View File
@@ -10,12 +10,11 @@ spec:
{{- include "garage.selectorLabels" . | nindent 6 }} {{- include "garage.selectorLabels" . | nindent 6 }}
{{- if eq .Values.deployment.kind "StatefulSet" }} {{- if eq .Values.deployment.kind "StatefulSet" }}
replicas: {{ .Values.deployment.replicaCount }} replicas: {{ .Values.deployment.replicaCount }}
serviceName: {{ include "garage.fullname" . }} serviceName: {{ include "garage.fullname" . }}-headless
podManagementPolicy: {{ .Values.deployment.podManagementPolicy }} podManagementPolicy: {{ .Values.deployment.podManagementPolicy }}
{{- end }} {{- end }}
template: template:
metadata: metadata:
annotations: annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }} {{- with .Values.podAnnotations }}
+2 -2
View File
@@ -1,12 +1,12 @@
[package] [package]
name = "garage_api_admin" name = "garage_api_admin"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
description = "Admin API server crate for the Garage object store" description = "Admin API server crate for the Garage object store"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage" repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
readme = "../../README.md" readme = "../../../README.md"
[lib] [lib]
path = "lib.rs" path = "lib.rs"
+7 -2
View File
@@ -1,12 +1,12 @@
[package] [package]
name = "garage_api_common" name = "garage_api_common"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
description = "Common functions for the API server crates for the Garage object store" description = "Common functions for the API server crates for the Garage object store"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage" repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
readme = "../../README.md" readme = "../../../README.md"
[lib] [lib]
path = "lib.rs" path = "lib.rs"
@@ -18,16 +18,21 @@ garage_model.workspace = true
garage_table.workspace = true garage_table.workspace = true
garage_util.workspace = true garage_util.workspace = true
base64.workspace = true
bytes.workspace = true bytes.workspace = true
chrono.workspace = true chrono.workspace = true
crc32fast.workspace = true
crc32c.workspace = true
crypto-common.workspace = true crypto-common.workspace = true
err-derive.workspace = true err-derive.workspace = true
hex.workspace = true hex.workspace = true
hmac.workspace = true hmac.workspace = true
md-5.workspace = true
idna.workspace = true idna.workspace = true
tracing.workspace = true tracing.workspace = true
nom.workspace = true nom.workspace = true
pin-project.workspace = true pin-project.workspace = true
sha1.workspace = true
sha2.workspace = true sha2.workspace = true
futures.workspace = true futures.workspace = true
+4 -4
View File
@@ -14,9 +14,9 @@ use crate::common_error::{
}; };
use crate::helpers::*; use crate::helpers::*;
pub fn find_matching_cors_rule<'a>( pub fn find_matching_cors_rule<'a, B>(
bucket_params: &'a BucketParams, bucket_params: &'a BucketParams,
req: &Request<impl Body>, req: &Request<B>,
) -> Result<Option<&'a GarageCorsRule>, CommonError> { ) -> Result<Option<&'a GarageCorsRule>, CommonError> {
if let Some(cors_config) = bucket_params.cors_config.get() { if let Some(cors_config) = bucket_params.cors_config.get() {
if let Some(origin) = req.headers().get("Origin") { if let Some(origin) = req.headers().get("Origin") {
@@ -132,8 +132,8 @@ pub async fn handle_options_api(
} }
} }
pub fn handle_options_for_bucket( pub fn handle_options_for_bucket<B>(
req: &Request<IncomingBody>, req: &Request<B>,
bucket_params: &BucketParams, bucket_params: &BucketParams,
) -> Result<Response<EmptyBody>, CommonError> { ) -> Result<Response<EmptyBody>, CommonError> {
let origin = req let origin = req
+135
View File
@@ -0,0 +1,135 @@
use std::sync::Mutex;
use futures::prelude::*;
use futures::stream::BoxStream;
use http_body_util::{BodyExt, StreamBody};
use hyper::body::{Bytes, Frame};
use serde::Deserialize;
use tokio::sync::mpsc;
use tokio::task;
use super::*;
use crate::signature::checksum::*;
pub struct ReqBody {
// why need mutex to be sync??
pub(crate) stream: Mutex<BoxStream<'static, Result<Frame<Bytes>, Error>>>,
pub(crate) checksummer: Checksummer,
pub(crate) expected_checksums: ExpectedChecksums,
pub(crate) trailer_algorithm: Option<ChecksumAlgorithm>,
}
pub type StreamingChecksumReceiver = task::JoinHandle<Result<Checksums, Error>>;
impl ReqBody {
pub fn add_expected_checksums(&mut self, more: ExpectedChecksums) {
if more.md5.is_some() {
self.expected_checksums.md5 = more.md5;
}
if more.sha256.is_some() {
self.expected_checksums.sha256 = more.sha256;
}
if more.extra.is_some() {
self.expected_checksums.extra = more.extra;
}
self.checksummer.add_expected(&self.expected_checksums);
}
pub fn add_md5(&mut self) {
self.checksummer.add_md5();
}
// ============ non-streaming =============
pub async fn json<T: for<'a> Deserialize<'a>>(self) -> Result<T, Error> {
let body = self.collect().await?;
let resp: T = serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?;
Ok(resp)
}
pub async fn collect(self) -> Result<Bytes, Error> {
self.collect_with_checksums().await.map(|(b, _)| b)
}
pub async fn collect_with_checksums(mut self) -> Result<(Bytes, Checksums), Error> {
let stream: BoxStream<_> = self.stream.into_inner().unwrap();
let bytes = BodyExt::collect(StreamBody::new(stream)).await?.to_bytes();
self.checksummer.update(&bytes);
let checksums = self.checksummer.finalize();
checksums.verify(&self.expected_checksums)?;
Ok((bytes, checksums))
}
// ============ streaming =============
pub fn streaming_with_checksums(
self,
) -> (
BoxStream<'static, Result<Bytes, Error>>,
StreamingChecksumReceiver,
) {
let Self {
stream,
mut checksummer,
mut expected_checksums,
trailer_algorithm,
} = self;
let (frame_tx, mut frame_rx) = mpsc::channel::<Frame<Bytes>>(5);
let join_checksums = tokio::spawn(async move {
while let Some(frame) = frame_rx.recv().await {
match frame.into_data() {
Ok(data) => {
checksummer = tokio::task::spawn_blocking(move || {
checksummer.update(&data);
checksummer
})
.await
.unwrap()
}
Err(frame) => {
let trailers = frame.into_trailers().unwrap();
let algo = trailer_algorithm.unwrap();
expected_checksums.extra = Some(extract_checksum_value(&trailers, algo)?);
break;
}
}
}
if trailer_algorithm.is_some() && expected_checksums.extra.is_none() {
return Err(Error::bad_request("trailing checksum was not sent"));
}
let checksums = checksummer.finalize();
checksums.verify(&expected_checksums)?;
Ok(checksums)
});
let stream: BoxStream<_> = stream.into_inner().unwrap();
let stream = stream.filter_map(move |x| {
let frame_tx = frame_tx.clone();
async move {
match x {
Err(e) => Some(Err(e)),
Ok(frame) => {
if frame.is_data() {
let data = frame.data_ref().unwrap().clone();
let _ = frame_tx.send(frame).await;
Some(Ok(data))
} else {
let _ = frame_tx.send(frame).await;
None
}
}
}
}
});
(stream.boxed(), join_checksums)
}
}
@@ -11,11 +11,12 @@ use sha2::Sha256;
use http::{HeaderMap, HeaderName, HeaderValue}; use http::{HeaderMap, HeaderName, HeaderValue};
use garage_util::data::*; use garage_util::data::*;
use garage_util::error::OkOrMessage;
use garage_model::s3::object_table::*; use super::*;
use crate::error::*; pub use garage_model::s3::object_table::{ChecksumAlgorithm, ChecksumValue};
pub const CONTENT_MD5: HeaderName = HeaderName::from_static("content-md5");
pub const X_AMZ_CHECKSUM_ALGORITHM: HeaderName = pub const X_AMZ_CHECKSUM_ALGORITHM: HeaderName =
HeaderName::from_static("x-amz-checksum-algorithm"); HeaderName::from_static("x-amz-checksum-algorithm");
@@ -31,8 +32,8 @@ pub type Md5Checksum = [u8; 16];
pub type Sha1Checksum = [u8; 20]; pub type Sha1Checksum = [u8; 20];
pub type Sha256Checksum = [u8; 32]; pub type Sha256Checksum = [u8; 32];
#[derive(Debug, Default)] #[derive(Debug, Default, Clone)]
pub(crate) struct ExpectedChecksums { pub struct ExpectedChecksums {
// base64-encoded md5 (content-md5 header) // base64-encoded md5 (content-md5 header)
pub md5: Option<String>, pub md5: Option<String>,
// content_sha256 (as a Hash / FixedBytes32) // content_sha256 (as a Hash / FixedBytes32)
@@ -41,7 +42,7 @@ pub(crate) struct ExpectedChecksums {
pub extra: Option<ChecksumValue>, pub extra: Option<ChecksumValue>,
} }
pub(crate) struct Checksummer { pub struct Checksummer {
pub crc32: Option<Crc32>, pub crc32: Option<Crc32>,
pub crc32c: Option<Crc32c>, pub crc32c: Option<Crc32c>,
pub md5: Option<Md5>, pub md5: Option<Md5>,
@@ -50,7 +51,7 @@ pub(crate) struct Checksummer {
} }
#[derive(Default)] #[derive(Default)]
pub(crate) struct Checksums { pub struct Checksums {
pub crc32: Option<Crc32Checksum>, pub crc32: Option<Crc32Checksum>,
pub crc32c: Option<Crc32cChecksum>, pub crc32c: Option<Crc32cChecksum>,
pub md5: Option<Md5Checksum>, pub md5: Option<Md5Checksum>,
@@ -59,34 +60,48 @@ pub(crate) struct Checksums {
} }
impl Checksummer { impl Checksummer {
pub(crate) fn init(expected: &ExpectedChecksums, require_md5: bool) -> Self { pub fn new() -> Self {
let mut ret = Self { Self {
crc32: None, crc32: None,
crc32c: None, crc32c: None,
md5: None, md5: None,
sha1: None, sha1: None,
sha256: None, sha256: None,
}; }
}
if expected.md5.is_some() || require_md5 { pub fn init(expected: &ExpectedChecksums, add_md5: bool) -> Self {
ret.md5 = Some(Md5::new()); let mut ret = Self::new();
} ret.add_expected(expected);
if expected.sha256.is_some() || matches!(&expected.extra, Some(ChecksumValue::Sha256(_))) { if add_md5 {
ret.sha256 = Some(Sha256::new()); ret.add_md5();
}
if matches!(&expected.extra, Some(ChecksumValue::Crc32(_))) {
ret.crc32 = Some(Crc32::new());
}
if matches!(&expected.extra, Some(ChecksumValue::Crc32c(_))) {
ret.crc32c = Some(Crc32c::default());
}
if matches!(&expected.extra, Some(ChecksumValue::Sha1(_))) {
ret.sha1 = Some(Sha1::new());
} }
ret ret
} }
pub(crate) fn add(mut self, algo: Option<ChecksumAlgorithm>) -> Self { pub fn add_md5(&mut self) {
self.md5 = Some(Md5::new());
}
pub fn add_expected(&mut self, expected: &ExpectedChecksums) {
if expected.md5.is_some() {
self.md5 = Some(Md5::new());
}
if expected.sha256.is_some() || matches!(&expected.extra, Some(ChecksumValue::Sha256(_))) {
self.sha256 = Some(Sha256::new());
}
if matches!(&expected.extra, Some(ChecksumValue::Crc32(_))) {
self.crc32 = Some(Crc32::new());
}
if matches!(&expected.extra, Some(ChecksumValue::Crc32c(_))) {
self.crc32c = Some(Crc32c::default());
}
if matches!(&expected.extra, Some(ChecksumValue::Sha1(_))) {
self.sha1 = Some(Sha1::new());
}
}
pub fn add(mut self, algo: Option<ChecksumAlgorithm>) -> Self {
match algo { match algo {
Some(ChecksumAlgorithm::Crc32) => { Some(ChecksumAlgorithm::Crc32) => {
self.crc32 = Some(Crc32::new()); self.crc32 = Some(Crc32::new());
@@ -105,7 +120,7 @@ impl Checksummer {
self self
} }
pub(crate) fn update(&mut self, bytes: &[u8]) { pub fn update(&mut self, bytes: &[u8]) {
if let Some(crc32) = &mut self.crc32 { if let Some(crc32) = &mut self.crc32 {
crc32.update(bytes); crc32.update(bytes);
} }
@@ -123,7 +138,7 @@ impl Checksummer {
} }
} }
pub(crate) fn finalize(self) -> Checksums { pub fn finalize(self) -> Checksums {
Checksums { Checksums {
crc32: self.crc32.map(|x| u32::to_be_bytes(x.finalize())), crc32: self.crc32.map(|x| u32::to_be_bytes(x.finalize())),
crc32c: self crc32c: self
@@ -183,153 +198,56 @@ impl Checksums {
// ---- // ----
#[derive(Default)] pub fn parse_checksum_algorithm(algo: &str) -> Result<ChecksumAlgorithm, Error> {
pub(crate) struct MultipartChecksummer { match algo {
pub md5: Md5, "CRC32" => Ok(ChecksumAlgorithm::Crc32),
pub extra: Option<MultipartExtraChecksummer>, "CRC32C" => Ok(ChecksumAlgorithm::Crc32c),
} "SHA1" => Ok(ChecksumAlgorithm::Sha1),
"SHA256" => Ok(ChecksumAlgorithm::Sha256),
pub(crate) enum MultipartExtraChecksummer { _ => Err(Error::bad_request("invalid checksum algorithm")),
Crc32(Crc32),
Crc32c(Crc32c),
Sha1(Sha1),
Sha256(Sha256),
}
impl MultipartChecksummer {
pub(crate) fn init(algo: Option<ChecksumAlgorithm>) -> Self {
Self {
md5: Md5::new(),
extra: match algo {
None => None,
Some(ChecksumAlgorithm::Crc32) => {
Some(MultipartExtraChecksummer::Crc32(Crc32::new()))
}
Some(ChecksumAlgorithm::Crc32c) => {
Some(MultipartExtraChecksummer::Crc32c(Crc32c::default()))
}
Some(ChecksumAlgorithm::Sha1) => Some(MultipartExtraChecksummer::Sha1(Sha1::new())),
Some(ChecksumAlgorithm::Sha256) => {
Some(MultipartExtraChecksummer::Sha256(Sha256::new()))
}
},
}
}
pub(crate) fn update(
&mut self,
etag: &str,
checksum: Option<ChecksumValue>,
) -> Result<(), Error> {
self.md5
.update(&hex::decode(&etag).ok_or_message("invalid etag hex")?);
match (&mut self.extra, checksum) {
(None, _) => (),
(
Some(MultipartExtraChecksummer::Crc32(ref mut crc32)),
Some(ChecksumValue::Crc32(x)),
) => {
crc32.update(&x);
}
(
Some(MultipartExtraChecksummer::Crc32c(ref mut crc32c)),
Some(ChecksumValue::Crc32c(x)),
) => {
crc32c.write(&x);
}
(Some(MultipartExtraChecksummer::Sha1(ref mut sha1)), Some(ChecksumValue::Sha1(x))) => {
sha1.update(&x);
}
(
Some(MultipartExtraChecksummer::Sha256(ref mut sha256)),
Some(ChecksumValue::Sha256(x)),
) => {
sha256.update(&x);
}
(Some(_), b) => {
return Err(Error::internal_error(format!(
"part checksum was not computed correctly, got: {:?}",
b
)))
}
}
Ok(())
}
pub(crate) fn finalize(self) -> (Md5Checksum, Option<ChecksumValue>) {
let md5 = self.md5.finalize()[..].try_into().unwrap();
let extra = match self.extra {
None => None,
Some(MultipartExtraChecksummer::Crc32(crc32)) => {
Some(ChecksumValue::Crc32(u32::to_be_bytes(crc32.finalize())))
}
Some(MultipartExtraChecksummer::Crc32c(crc32c)) => Some(ChecksumValue::Crc32c(
u32::to_be_bytes(u32::try_from(crc32c.finish()).unwrap()),
)),
Some(MultipartExtraChecksummer::Sha1(sha1)) => {
Some(ChecksumValue::Sha1(sha1.finalize()[..].try_into().unwrap()))
}
Some(MultipartExtraChecksummer::Sha256(sha256)) => Some(ChecksumValue::Sha256(
sha256.finalize()[..].try_into().unwrap(),
)),
};
(md5, extra)
} }
} }
// ----
/// Extract the value of the x-amz-checksum-algorithm header /// Extract the value of the x-amz-checksum-algorithm header
pub(crate) fn request_checksum_algorithm( pub fn request_checksum_algorithm(
headers: &HeaderMap<HeaderValue>, headers: &HeaderMap<HeaderValue>,
) -> Result<Option<ChecksumAlgorithm>, Error> { ) -> Result<Option<ChecksumAlgorithm>, Error> {
match headers.get(X_AMZ_CHECKSUM_ALGORITHM) { match headers.get(X_AMZ_CHECKSUM_ALGORITHM) {
None => Ok(None), None => Ok(None),
Some(x) if x == "CRC32" => Ok(Some(ChecksumAlgorithm::Crc32)), Some(x) => parse_checksum_algorithm(x.to_str()?).map(Some),
Some(x) if x == "CRC32C" => Ok(Some(ChecksumAlgorithm::Crc32c)), }
Some(x) if x == "SHA1" => Ok(Some(ChecksumAlgorithm::Sha1)), }
Some(x) if x == "SHA256" => Ok(Some(ChecksumAlgorithm::Sha256)),
pub fn request_trailer_checksum_algorithm(
headers: &HeaderMap<HeaderValue>,
) -> Result<Option<ChecksumAlgorithm>, Error> {
match headers.get(X_AMZ_TRAILER).map(|x| x.to_str()).transpose()? {
None => Ok(None),
Some(x) if x == X_AMZ_CHECKSUM_CRC32 => Ok(Some(ChecksumAlgorithm::Crc32)),
Some(x) if x == X_AMZ_CHECKSUM_CRC32C => Ok(Some(ChecksumAlgorithm::Crc32c)),
Some(x) if x == X_AMZ_CHECKSUM_SHA1 => Ok(Some(ChecksumAlgorithm::Sha1)),
Some(x) if x == X_AMZ_CHECKSUM_SHA256 => Ok(Some(ChecksumAlgorithm::Sha256)),
_ => Err(Error::bad_request("invalid checksum algorithm")), _ => Err(Error::bad_request("invalid checksum algorithm")),
} }
} }
/// Extract the value of any of the x-amz-checksum-* headers /// Extract the value of any of the x-amz-checksum-* headers
pub(crate) fn request_checksum_value( pub fn request_checksum_value(
headers: &HeaderMap<HeaderValue>, headers: &HeaderMap<HeaderValue>,
) -> Result<Option<ChecksumValue>, Error> { ) -> Result<Option<ChecksumValue>, Error> {
let mut ret = vec![]; let mut ret = vec![];
if let Some(crc32_str) = headers.get(X_AMZ_CHECKSUM_CRC32) { if headers.contains_key(X_AMZ_CHECKSUM_CRC32) {
let crc32 = BASE64_STANDARD ret.push(extract_checksum_value(headers, ChecksumAlgorithm::Crc32)?);
.decode(&crc32_str)
.ok()
.and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-crc32 header")?;
ret.push(ChecksumValue::Crc32(crc32))
} }
if let Some(crc32c_str) = headers.get(X_AMZ_CHECKSUM_CRC32C) { if headers.contains_key(X_AMZ_CHECKSUM_CRC32C) {
let crc32c = BASE64_STANDARD ret.push(extract_checksum_value(headers, ChecksumAlgorithm::Crc32c)?);
.decode(&crc32c_str)
.ok()
.and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-crc32c header")?;
ret.push(ChecksumValue::Crc32c(crc32c))
} }
if let Some(sha1_str) = headers.get(X_AMZ_CHECKSUM_SHA1) { if headers.contains_key(X_AMZ_CHECKSUM_SHA1) {
let sha1 = BASE64_STANDARD ret.push(extract_checksum_value(headers, ChecksumAlgorithm::Sha1)?);
.decode(&sha1_str)
.ok()
.and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-sha1 header")?;
ret.push(ChecksumValue::Sha1(sha1))
} }
if let Some(sha256_str) = headers.get(X_AMZ_CHECKSUM_SHA256) { if headers.contains_key(X_AMZ_CHECKSUM_SHA256) {
let sha256 = BASE64_STANDARD ret.push(extract_checksum_value(headers, ChecksumAlgorithm::Sha256)?);
.decode(&sha256_str)
.ok()
.and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-sha256 header")?;
ret.push(ChecksumValue::Sha256(sha256))
} }
if ret.len() > 1 { if ret.len() > 1 {
@@ -342,48 +260,47 @@ pub(crate) fn request_checksum_value(
/// Checks for the presence of x-amz-checksum-algorithm /// Checks for the presence of x-amz-checksum-algorithm
/// if so extract the corresponding x-amz-checksum-* value /// if so extract the corresponding x-amz-checksum-* value
pub(crate) fn request_checksum_algorithm_value( pub fn extract_checksum_value(
headers: &HeaderMap<HeaderValue>, headers: &HeaderMap<HeaderValue>,
) -> Result<Option<ChecksumValue>, Error> { algo: ChecksumAlgorithm,
match headers.get(X_AMZ_CHECKSUM_ALGORITHM) { ) -> Result<ChecksumValue, Error> {
Some(x) if x == "CRC32" => { match algo {
ChecksumAlgorithm::Crc32 => {
let crc32 = headers let crc32 = headers
.get(X_AMZ_CHECKSUM_CRC32) .get(X_AMZ_CHECKSUM_CRC32)
.and_then(|x| BASE64_STANDARD.decode(&x).ok()) .and_then(|x| BASE64_STANDARD.decode(&x).ok())
.and_then(|x| x.try_into().ok()) .and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-crc32 header")?; .ok_or_bad_request("invalid x-amz-checksum-crc32 header")?;
Ok(Some(ChecksumValue::Crc32(crc32))) Ok(ChecksumValue::Crc32(crc32))
} }
Some(x) if x == "CRC32C" => { ChecksumAlgorithm::Crc32c => {
let crc32c = headers let crc32c = headers
.get(X_AMZ_CHECKSUM_CRC32C) .get(X_AMZ_CHECKSUM_CRC32C)
.and_then(|x| BASE64_STANDARD.decode(&x).ok()) .and_then(|x| BASE64_STANDARD.decode(&x).ok())
.and_then(|x| x.try_into().ok()) .and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-crc32c header")?; .ok_or_bad_request("invalid x-amz-checksum-crc32c header")?;
Ok(Some(ChecksumValue::Crc32c(crc32c))) Ok(ChecksumValue::Crc32c(crc32c))
} }
Some(x) if x == "SHA1" => { ChecksumAlgorithm::Sha1 => {
let sha1 = headers let sha1 = headers
.get(X_AMZ_CHECKSUM_SHA1) .get(X_AMZ_CHECKSUM_SHA1)
.and_then(|x| BASE64_STANDARD.decode(&x).ok()) .and_then(|x| BASE64_STANDARD.decode(&x).ok())
.and_then(|x| x.try_into().ok()) .and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-sha1 header")?; .ok_or_bad_request("invalid x-amz-checksum-sha1 header")?;
Ok(Some(ChecksumValue::Sha1(sha1))) Ok(ChecksumValue::Sha1(sha1))
} }
Some(x) if x == "SHA256" => { ChecksumAlgorithm::Sha256 => {
let sha256 = headers let sha256 = headers
.get(X_AMZ_CHECKSUM_SHA256) .get(X_AMZ_CHECKSUM_SHA256)
.and_then(|x| BASE64_STANDARD.decode(&x).ok()) .and_then(|x| BASE64_STANDARD.decode(&x).ok())
.and_then(|x| x.try_into().ok()) .and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-sha256 header")?; .ok_or_bad_request("invalid x-amz-checksum-sha256 header")?;
Ok(Some(ChecksumValue::Sha256(sha256))) Ok(ChecksumValue::Sha256(sha256))
} }
Some(_) => Err(Error::bad_request("invalid x-amz-checksum-algorithm")),
None => Ok(None),
} }
} }
pub(crate) fn add_checksum_response_headers( pub fn add_checksum_response_headers(
checksum: &Option<ChecksumValue>, checksum: &Option<ChecksumValue>,
mut resp: http::response::Builder, mut resp: http::response::Builder,
) -> http::response::Builder { ) -> http::response::Builder {
+4
View File
@@ -18,6 +18,10 @@ pub enum Error {
/// The request contained an invalid UTF-8 sequence in its path or in other parameters /// The request contained an invalid UTF-8 sequence in its path or in other parameters
#[error(display = "Invalid UTF-8: {}", _0)] #[error(display = "Invalid UTF-8: {}", _0)]
InvalidUtf8Str(#[error(source)] std::str::Utf8Error), InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
/// The provided digest (checksum) value was invalid
#[error(display = "Invalid digest: {}", _0)]
InvalidDigest(String),
} }
impl<T> From<T> for Error impl<T> From<T> for Error
+57 -17
View File
@@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use sha2::Sha256; use sha2::Sha256;
use hyper::header::HeaderName;
use hyper::{body::Incoming as IncomingBody, Request}; use hyper::{body::Incoming as IncomingBody, Request};
use garage_model::garage::Garage; use garage_model::garage::Garage;
@@ -10,6 +11,8 @@ use garage_util::data::{sha256sum, Hash};
use error::*; use error::*;
pub mod body;
pub mod checksum;
pub mod error; pub mod error;
pub mod payload; pub mod payload;
pub mod streaming; pub mod streaming;
@@ -17,36 +20,73 @@ pub mod streaming;
pub const SHORT_DATE: &str = "%Y%m%d"; pub const SHORT_DATE: &str = "%Y%m%d";
pub const LONG_DATETIME: &str = "%Y%m%dT%H%M%SZ"; pub const LONG_DATETIME: &str = "%Y%m%dT%H%M%SZ";
// ---- Constants used in AWSv4 signatures ----
pub const X_AMZ_ALGORITHM: HeaderName = HeaderName::from_static("x-amz-algorithm");
pub const X_AMZ_CREDENTIAL: HeaderName = HeaderName::from_static("x-amz-credential");
pub const X_AMZ_DATE: HeaderName = HeaderName::from_static("x-amz-date");
pub const X_AMZ_EXPIRES: HeaderName = HeaderName::from_static("x-amz-expires");
pub const X_AMZ_SIGNEDHEADERS: HeaderName = HeaderName::from_static("x-amz-signedheaders");
pub const X_AMZ_SIGNATURE: HeaderName = HeaderName::from_static("x-amz-signature");
pub const X_AMZ_CONTENT_SHA256: HeaderName = HeaderName::from_static("x-amz-content-sha256");
pub const X_AMZ_TRAILER: HeaderName = HeaderName::from_static("x-amz-trailer");
/// Result of `sha256("")`
pub(crate) const EMPTY_STRING_HEX_DIGEST: &str =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
// Signature calculation algorithm
pub const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256";
type HmacSha256 = Hmac<Sha256>; type HmacSha256 = Hmac<Sha256>;
// Possible values for x-amz-content-sha256, in addition to the actual sha256
pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
pub const STREAMING_UNSIGNED_PAYLOAD_TRAILER: &str = "STREAMING-UNSIGNED-PAYLOAD-TRAILER";
pub const STREAMING_AWS4_HMAC_SHA256_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
// Used in the computation of StringToSign
pub const AWS4_HMAC_SHA256_PAYLOAD: &str = "AWS4-HMAC-SHA256-PAYLOAD";
// ---- enums to describe stuff going on in signature calculation ----
#[derive(Debug)]
pub enum ContentSha256Header {
UnsignedPayload,
Sha256Checksum(Hash),
StreamingPayload { trailer: bool, signed: bool },
}
// ---- top-level functions ----
pub struct VerifiedRequest {
pub request: Request<streaming::ReqBody>,
pub access_key: Key,
pub content_sha256_header: ContentSha256Header,
}
pub async fn verify_request( pub async fn verify_request(
garage: &Garage, garage: &Garage,
mut req: Request<IncomingBody>, mut req: Request<IncomingBody>,
service: &'static str, service: &'static str,
) -> Result<(Request<streaming::ReqBody>, Key, Option<Hash>), Error> { ) -> Result<VerifiedRequest, Error> {
let (api_key, mut content_sha256) = let checked_signature = payload::check_payload_signature(&garage, &mut req, service).await?;
payload::check_payload_signature(&garage, &mut req, service).await?;
let api_key =
api_key.ok_or_else(|| Error::forbidden("Garage does not support anonymous access yet"))?;
let req = streaming::parse_streaming_body( let request = streaming::parse_streaming_body(
&api_key,
req, req,
&mut content_sha256, &checked_signature,
&garage.config.s3_api.s3_region, &garage.config.s3_api.s3_region,
service, service,
)?; )?;
Ok((req, api_key, content_sha256)) let access_key = checked_signature
} .key
.ok_or_else(|| Error::forbidden("Garage does not support anonymous access yet"))?;
pub fn verify_signed_content(expected_sha256: Hash, body: &[u8]) -> Result<(), Error> { Ok(VerifiedRequest {
if expected_sha256 != sha256sum(body) { request,
return Err(Error::bad_request( access_key,
"Request content hash does not match signed hash".to_string(), content_sha256_header: checked_signature.content_sha256_header,
)); })
}
Ok(())
} }
pub fn signing_hmac( pub fn signing_hmac(
+62 -41
View File
@@ -13,23 +13,9 @@ use garage_util::data::Hash;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::key_table::*; use garage_model::key_table::*;
use super::LONG_DATETIME; use super::*;
use super::{compute_scope, signing_hmac};
use crate::encoding::uri_encode; use crate::encoding::uri_encode;
use crate::signature::error::*;
pub const X_AMZ_ALGORITHM: HeaderName = HeaderName::from_static("x-amz-algorithm");
pub const X_AMZ_CREDENTIAL: HeaderName = HeaderName::from_static("x-amz-credential");
pub const X_AMZ_DATE: HeaderName = HeaderName::from_static("x-amz-date");
pub const X_AMZ_EXPIRES: HeaderName = HeaderName::from_static("x-amz-expires");
pub const X_AMZ_SIGNEDHEADERS: HeaderName = HeaderName::from_static("x-amz-signedheaders");
pub const X_AMZ_SIGNATURE: HeaderName = HeaderName::from_static("x-amz-signature");
pub const X_AMZ_CONTENT_SH256: HeaderName = HeaderName::from_static("x-amz-content-sha256");
pub const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256";
pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
pub const STREAMING_AWS4_HMAC_SHA256_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
pub type QueryMap = HeaderMap<QueryValue>; pub type QueryMap = HeaderMap<QueryValue>;
pub struct QueryValue { pub struct QueryValue {
@@ -39,11 +25,18 @@ pub struct QueryValue {
value: String, value: String,
} }
#[derive(Debug)]
pub struct CheckedSignature {
pub key: Option<Key>,
pub content_sha256_header: ContentSha256Header,
pub signature_header: Option<String>,
}
pub async fn check_payload_signature( pub async fn check_payload_signature(
garage: &Garage, garage: &Garage,
request: &mut Request<IncomingBody>, request: &mut Request<IncomingBody>,
service: &'static str, service: &'static str,
) -> Result<(Option<Key>, Option<Hash>), Error> { ) -> Result<CheckedSignature, Error> {
let query = parse_query_map(request.uri())?; let query = parse_query_map(request.uri())?;
if query.contains_key(&X_AMZ_ALGORITHM) { if query.contains_key(&X_AMZ_ALGORITHM) {
@@ -57,17 +50,46 @@ pub async fn check_payload_signature(
// Unsigned (anonymous) request // Unsigned (anonymous) request
let content_sha256 = request let content_sha256 = request
.headers() .headers()
.get("x-amz-content-sha256") .get(X_AMZ_CONTENT_SHA256)
.filter(|c| c.as_bytes() != UNSIGNED_PAYLOAD.as_bytes()); .map(|x| x.to_str())
if let Some(content_sha256) = content_sha256 { .transpose()?;
let sha256 = hex::decode(content_sha256) Ok(CheckedSignature {
.ok() key: None,
.and_then(|bytes| Hash::try_from(&bytes)) content_sha256_header: parse_x_amz_content_sha256(content_sha256)?,
.ok_or_bad_request("Invalid content sha256 hash")?; signature_header: None,
Ok((None, Some(sha256))) })
}
}
fn parse_x_amz_content_sha256(header: Option<&str>) -> Result<ContentSha256Header, Error> {
let header = match header {
Some(x) => x,
None => return Ok(ContentSha256Header::UnsignedPayload),
};
if header == UNSIGNED_PAYLOAD {
Ok(ContentSha256Header::UnsignedPayload)
} else if let Some(rest) = header.strip_prefix("STREAMING-") {
let (trailer, algo) = if let Some(rest2) = rest.strip_suffix("-TRAILER") {
(true, rest2)
} else { } else {
Ok((None, None)) (false, rest)
} };
let signed = match algo {
AWS4_HMAC_SHA256_PAYLOAD => true,
UNSIGNED_PAYLOAD => false,
_ => {
return Err(Error::bad_request(
"invalid or unsupported x-amz-content-sha256",
))
}
};
Ok(ContentSha256Header::StreamingPayload { trailer, signed })
} else {
let sha256 = hex::decode(header)
.ok()
.and_then(|bytes| Hash::try_from(&bytes))
.ok_or_bad_request("Invalid content sha256 hash")?;
Ok(ContentSha256Header::Sha256Checksum(sha256))
} }
} }
@@ -76,7 +98,7 @@ async fn check_standard_signature(
service: &'static str, service: &'static str,
request: &Request<IncomingBody>, request: &Request<IncomingBody>,
query: QueryMap, query: QueryMap,
) -> Result<(Option<Key>, Option<Hash>), Error> { ) -> Result<CheckedSignature, Error> {
let authorization = Authorization::parse_header(request.headers())?; let authorization = Authorization::parse_header(request.headers())?;
// Verify that all necessary request headers are included in signed_headers // Verify that all necessary request headers are included in signed_headers
@@ -108,18 +130,13 @@ async fn check_standard_signature(
let key = verify_v4(garage, service, &authorization, string_to_sign.as_bytes()).await?; let key = verify_v4(garage, service, &authorization, string_to_sign.as_bytes()).await?;
let content_sha256 = if authorization.content_sha256 == UNSIGNED_PAYLOAD { let content_sha256_header = parse_x_amz_content_sha256(Some(&authorization.content_sha256))?;
None
} else if authorization.content_sha256 == STREAMING_AWS4_HMAC_SHA256_PAYLOAD {
let bytes = hex::decode(authorization.signature).ok_or_bad_request("Invalid signature")?;
Some(Hash::try_from(&bytes).ok_or_bad_request("Invalid signature")?)
} else {
let bytes = hex::decode(authorization.content_sha256)
.ok_or_bad_request("Invalid content sha256 hash")?;
Some(Hash::try_from(&bytes).ok_or_bad_request("Invalid content sha256 hash")?)
};
Ok((Some(key), content_sha256)) Ok(CheckedSignature {
key: Some(key),
content_sha256_header,
signature_header: Some(authorization.signature),
})
} }
async fn check_presigned_signature( async fn check_presigned_signature(
@@ -127,7 +144,7 @@ async fn check_presigned_signature(
service: &'static str, service: &'static str,
request: &mut Request<IncomingBody>, request: &mut Request<IncomingBody>,
mut query: QueryMap, mut query: QueryMap,
) -> Result<(Option<Key>, Option<Hash>), Error> { ) -> Result<CheckedSignature, Error> {
let algorithm = query.get(&X_AMZ_ALGORITHM).unwrap(); let algorithm = query.get(&X_AMZ_ALGORITHM).unwrap();
let authorization = Authorization::parse_presigned(&algorithm.value, &query)?; let authorization = Authorization::parse_presigned(&algorithm.value, &query)?;
@@ -193,7 +210,11 @@ async fn check_presigned_signature(
// Presigned URLs always use UNSIGNED-PAYLOAD, // Presigned URLs always use UNSIGNED-PAYLOAD,
// so there is no sha256 hash to return. // so there is no sha256 hash to return.
Ok((Some(key), None)) Ok(CheckedSignature {
key: Some(key),
content_sha256_header: ContentSha256Header::UnsignedPayload,
signature_header: Some(authorization.signature),
})
} }
pub fn parse_query_map(uri: &http::uri::Uri) -> Result<QueryMap, Error> { pub fn parse_query_map(uri: &http::uri::Uri) -> Result<QueryMap, Error> {
@@ -442,7 +463,7 @@ impl Authorization {
.to_string(); .to_string();
let content_sha256 = headers let content_sha256 = headers
.get(X_AMZ_CONTENT_SH256) .get(X_AMZ_CONTENT_SHA256)
.ok_or_bad_request("Missing X-Amz-Content-Sha256 field")?; .ok_or_bad_request("Missing X-Amz-Content-Sha256 field")?;
let date = headers let date = headers
+402 -157
View File
@@ -1,84 +1,157 @@
use std::pin::Pin; use std::pin::Pin;
use std::sync::Mutex;
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
use futures::prelude::*; use futures::prelude::*;
use futures::task; use futures::task;
use garage_model::key_table::Key;
use hmac::Mac; use hmac::Mac;
use http_body_util::StreamBody; use http::header::{HeaderMap, HeaderValue, CONTENT_ENCODING};
use hyper::body::{Bytes, Incoming as IncomingBody}; use hyper::body::{Bytes, Frame, Incoming as IncomingBody};
use hyper::Request; use hyper::Request;
use garage_util::data::Hash; use garage_util::data::Hash;
use super::{compute_scope, sha256sum, HmacSha256, LONG_DATETIME}; use super::*;
use crate::helpers::*; use crate::helpers::body_stream;
use crate::signature::error::*; use crate::signature::checksum::*;
use crate::signature::payload::{ use crate::signature::payload::CheckedSignature;
STREAMING_AWS4_HMAC_SHA256_PAYLOAD, X_AMZ_CONTENT_SH256, X_AMZ_DATE,
};
pub const AWS4_HMAC_SHA256_PAYLOAD: &str = "AWS4-HMAC-SHA256-PAYLOAD"; pub use crate::signature::body::ReqBody;
pub type ReqBody = BoxBody<Error>;
pub fn parse_streaming_body( pub fn parse_streaming_body(
api_key: &Key, mut req: Request<IncomingBody>,
req: Request<IncomingBody>, checked_signature: &CheckedSignature,
content_sha256: &mut Option<Hash>,
region: &str, region: &str,
service: &str, service: &str,
) -> Result<Request<ReqBody>, Error> { ) -> Result<Request<ReqBody>, Error> {
match req.headers().get(X_AMZ_CONTENT_SH256) { debug!(
Some(header) if header == STREAMING_AWS4_HMAC_SHA256_PAYLOAD => { "Content signature mode: {:?}",
let signature = content_sha256 checked_signature.content_sha256_header
.take() );
.ok_or_bad_request("No signature provided")?;
let secret_key = &api_key match checked_signature.content_sha256_header {
.state ContentSha256Header::StreamingPayload { signed, trailer } => {
.as_option() // Sanity checks
.ok_or_internal_error("Deleted key state")? if !signed && !trailer {
.secret_key; return Err(Error::bad_request(
"STREAMING-UNSIGNED-PAYLOAD without trailer is not a valid combination",
));
}
let date = req // Remove the aws-chunked component in the content-encoding: header
.headers() // Note: this header is not properly sent by minio client, so don't fail
.get(X_AMZ_DATE) // if it is absent from the request.
.ok_or_bad_request("Missing X-Amz-Date field")? if let Some(content_encoding) = req.headers_mut().remove(CONTENT_ENCODING) {
.to_str()?; if let Some(rest) = content_encoding.as_bytes().strip_prefix(b"aws-chunked,") {
let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, LONG_DATETIME) req.headers_mut()
.ok_or_bad_request("Invalid date")?; .insert(CONTENT_ENCODING, HeaderValue::from_bytes(rest).unwrap());
let date: DateTime<Utc> = Utc.from_utc_datetime(&date); } else if content_encoding != "aws-chunked" {
return Err(Error::bad_request(
"content-encoding does not contain aws-chunked for STREAMING-*-PAYLOAD",
));
}
}
let scope = compute_scope(&date, region, service); // If trailer header is announced, add the calculation of the requested checksum
let signing_hmac = crate::signature::signing_hmac(&date, secret_key, region, service) let mut checksummer = Checksummer::init(&Default::default(), false);
.ok_or_internal_error("Unable to build signing HMAC")?; let trailer_algorithm = if trailer {
let algo = Some(
request_trailer_checksum_algorithm(req.headers())?
.ok_or_bad_request("Missing x-amz-trailer header")?,
);
checksummer = checksummer.add(algo);
algo
} else {
None
};
// For signed variants, determine signing parameters
let sign_params = if signed {
let signature = checked_signature
.signature_header
.clone()
.ok_or_bad_request("No signature provided")?;
let signature = hex::decode(signature)
.ok()
.and_then(|bytes| Hash::try_from(&bytes))
.ok_or_bad_request("Invalid signature")?;
let secret_key = checked_signature
.key
.as_ref()
.ok_or_bad_request("Cannot sign streaming payload without signing key")?
.state
.as_option()
.ok_or_internal_error("Deleted key state")?
.secret_key
.to_string();
let date = req
.headers()
.get(X_AMZ_DATE)
.ok_or_bad_request("Missing X-Amz-Date field")?
.to_str()?;
let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, LONG_DATETIME)
.ok_or_bad_request("Invalid date")?;
let date: DateTime<Utc> = Utc.from_utc_datetime(&date);
let scope = compute_scope(&date, region, service);
let signing_hmac =
crate::signature::signing_hmac(&date, &secret_key, region, service)
.ok_or_internal_error("Unable to build signing HMAC")?;
Some(SignParams {
datetime: date,
scope,
signing_hmac,
previous_signature: signature,
})
} else {
None
};
Ok(req.map(move |body| { Ok(req.map(move |body| {
let stream = body_stream::<_, Error>(body); let stream = body_stream::<_, Error>(body);
let signed_payload_stream = let signed_payload_stream =
SignedPayloadStream::new(stream, signing_hmac, date, &scope, signature) StreamingPayloadStream::new(stream, sign_params, trailer).map_err(Error::from);
.map(|x| x.map(hyper::body::Frame::data)) ReqBody {
.map_err(Error::from); stream: Mutex::new(signed_payload_stream.boxed()),
ReqBody::new(StreamBody::new(signed_payload_stream)) checksummer,
expected_checksums: Default::default(),
trailer_algorithm,
}
})) }))
} }
_ => Ok(req.map(|body| ReqBody::new(http_body_util::BodyExt::map_err(body, Error::from)))), _ => Ok(req.map(|body| {
let expected_checksums = ExpectedChecksums {
sha256: match &checked_signature.content_sha256_header {
ContentSha256Header::Sha256Checksum(sha256) => Some(*sha256),
_ => None,
},
..Default::default()
};
let checksummer = Checksummer::init(&expected_checksums, false);
let stream = http_body_util::BodyStream::new(body).map_err(Error::from);
ReqBody {
stream: Mutex::new(stream.boxed()),
checksummer,
expected_checksums,
trailer_algorithm: None,
}
})),
} }
} }
/// Result of `sha256("")`
const EMPTY_STRING_HEX_DIGEST: &str =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
fn compute_streaming_payload_signature( fn compute_streaming_payload_signature(
signing_hmac: &HmacSha256, signing_hmac: &HmacSha256,
date: DateTime<Utc>, date: DateTime<Utc>,
scope: &str, scope: &str,
previous_signature: Hash, previous_signature: Hash,
content_sha256: Hash, content_sha256: Hash,
) -> Result<Hash, Error> { ) -> Result<Hash, StreamingPayloadError> {
let string_to_sign = [ let string_to_sign = [
AWS4_HMAC_SHA256_PAYLOAD, AWS4_HMAC_SHA256_PAYLOAD,
&date.format(LONG_DATETIME).to_string(), &date.format(LONG_DATETIME).to_string(),
@@ -92,12 +165,49 @@ fn compute_streaming_payload_signature(
let mut hmac = signing_hmac.clone(); let mut hmac = signing_hmac.clone();
hmac.update(string_to_sign.as_bytes()); hmac.update(string_to_sign.as_bytes());
Ok(Hash::try_from(&hmac.finalize().into_bytes()).ok_or_internal_error("Invalid signature")?) Hash::try_from(&hmac.finalize().into_bytes())
.ok_or_else(|| StreamingPayloadError::Message("Could not build signature".into()))
}
fn compute_streaming_trailer_signature(
signing_hmac: &HmacSha256,
date: DateTime<Utc>,
scope: &str,
previous_signature: Hash,
trailer_sha256: Hash,
) -> Result<Hash, StreamingPayloadError> {
let string_to_sign = [
AWS4_HMAC_SHA256_PAYLOAD,
&date.format(LONG_DATETIME).to_string(),
scope,
&hex::encode(previous_signature),
&hex::encode(trailer_sha256),
]
.join("\n");
let mut hmac = signing_hmac.clone();
hmac.update(string_to_sign.as_bytes());
Hash::try_from(&hmac.finalize().into_bytes())
.ok_or_else(|| StreamingPayloadError::Message("Could not build signature".into()))
} }
mod payload { mod payload {
use http::{HeaderName, HeaderValue};
use garage_util::data::Hash; use garage_util::data::Hash;
use nom::bytes::streaming::{tag, take_while};
use nom::character::streaming::hex_digit1;
use nom::combinator::{map_res, opt};
use nom::number::streaming::hex_u32;
macro_rules! try_parse {
($expr:expr) => {
$expr.map_err(|e| e.map(Error::Parser))?
};
}
pub enum Error<I> { pub enum Error<I> {
Parser(nom::error::Error<I>), Parser(nom::error::Error<I>),
BadSignature, BadSignature,
@@ -113,24 +223,13 @@ mod payload {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Header { pub struct ChunkHeader {
pub size: usize, pub size: usize,
pub signature: Hash, pub signature: Option<Hash>,
} }
impl Header { impl ChunkHeader {
pub fn parse(input: &[u8]) -> nom::IResult<&[u8], Self, Error<&[u8]>> { pub fn parse_signed(input: &[u8]) -> nom::IResult<&[u8], Self, Error<&[u8]>> {
use nom::bytes::streaming::tag;
use nom::character::streaming::hex_digit1;
use nom::combinator::map_res;
use nom::number::streaming::hex_u32;
macro_rules! try_parse {
($expr:expr) => {
$expr.map_err(|e| e.map(Error::Parser))?
};
}
let (input, size) = try_parse!(hex_u32(input)); let (input, size) = try_parse!(hex_u32(input));
let (input, _) = try_parse!(tag(";")(input)); let (input, _) = try_parse!(tag(";")(input));
@@ -140,96 +239,172 @@ mod payload {
let (input, _) = try_parse!(tag("\r\n")(input)); let (input, _) = try_parse!(tag("\r\n")(input));
let header = Header { let header = ChunkHeader {
size: size as usize, size: size as usize,
signature, signature: Some(signature),
};
Ok((input, header))
}
pub fn parse_unsigned(input: &[u8]) -> nom::IResult<&[u8], Self, Error<&[u8]>> {
let (input, size) = try_parse!(hex_u32(input));
let (input, _) = try_parse!(tag("\r\n")(input));
let header = ChunkHeader {
size: size as usize,
signature: None,
}; };
Ok((input, header)) Ok((input, header))
} }
} }
#[derive(Debug, Clone)]
pub struct TrailerChunk {
pub header_name: HeaderName,
pub header_value: HeaderValue,
pub signature: Option<Hash>,
}
impl TrailerChunk {
fn parse_content(input: &[u8]) -> nom::IResult<&[u8], Self, Error<&[u8]>> {
let (input, header_name) = try_parse!(map_res(
take_while(|c: u8| c.is_ascii_alphanumeric() || c == b'-'),
HeaderName::from_bytes
)(input));
let (input, _) = try_parse!(tag(b":")(input));
let (input, header_value) = try_parse!(map_res(
take_while(|c: u8| c.is_ascii_alphanumeric() || b"+/=".contains(&c)),
HeaderValue::from_bytes
)(input));
// Possible '\n' after the header value, depends on clients
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html
let (input, _) = try_parse!(opt(tag(b"\n"))(input));
let (input, _) = try_parse!(tag(b"\r\n")(input));
Ok((
input,
TrailerChunk {
header_name,
header_value,
signature: None,
},
))
}
pub fn parse_signed(input: &[u8]) -> nom::IResult<&[u8], Self, Error<&[u8]>> {
let (input, trailer) = Self::parse_content(input)?;
let (input, _) = try_parse!(tag(b"x-amz-trailer-signature:")(input));
let (input, data) = try_parse!(map_res(hex_digit1, hex::decode)(input));
let signature = Hash::try_from(&data).ok_or(nom::Err::Failure(Error::BadSignature))?;
let (input, _) = try_parse!(tag(b"\r\n")(input));
Ok((
input,
TrailerChunk {
signature: Some(signature),
..trailer
},
))
}
pub fn parse_unsigned(input: &[u8]) -> nom::IResult<&[u8], Self, Error<&[u8]>> {
let (input, trailer) = Self::parse_content(input)?;
let (input, _) = try_parse!(tag(b"\r\n")(input));
Ok((input, trailer))
}
}
} }
#[derive(Debug)] #[derive(Debug)]
pub enum SignedPayloadStreamError { pub enum StreamingPayloadError {
Stream(Error), Stream(Error),
InvalidSignature, InvalidSignature,
Message(String), Message(String),
} }
impl SignedPayloadStreamError { impl StreamingPayloadError {
fn message(msg: &str) -> Self { fn message(msg: &str) -> Self {
SignedPayloadStreamError::Message(msg.into()) StreamingPayloadError::Message(msg.into())
} }
} }
impl From<SignedPayloadStreamError> for Error { impl From<StreamingPayloadError> for Error {
fn from(err: SignedPayloadStreamError) -> Self { fn from(err: StreamingPayloadError) -> Self {
match err { match err {
SignedPayloadStreamError::Stream(e) => e, StreamingPayloadError::Stream(e) => e,
SignedPayloadStreamError::InvalidSignature => { StreamingPayloadError::InvalidSignature => {
Error::bad_request("Invalid payload signature") Error::bad_request("Invalid payload signature")
} }
SignedPayloadStreamError::Message(e) => { StreamingPayloadError::Message(e) => {
Error::bad_request(format!("Chunk format error: {}", e)) Error::bad_request(format!("Chunk format error: {}", e))
} }
} }
} }
} }
impl<I> From<payload::Error<I>> for SignedPayloadStreamError { impl<I> From<payload::Error<I>> for StreamingPayloadError {
fn from(err: payload::Error<I>) -> Self { fn from(err: payload::Error<I>) -> Self {
Self::message(err.description()) Self::message(err.description())
} }
} }
impl<I> From<nom::error::Error<I>> for SignedPayloadStreamError { impl<I> From<nom::error::Error<I>> for StreamingPayloadError {
fn from(err: nom::error::Error<I>) -> Self { fn from(err: nom::error::Error<I>) -> Self {
Self::message(err.code.description()) Self::message(err.code.description())
} }
} }
struct SignedPayload { enum StreamingPayloadChunk {
header: payload::Header, Chunk {
data: Bytes, header: payload::ChunkHeader,
data: Bytes,
},
Trailer(payload::TrailerChunk),
} }
#[pin_project::pin_project] struct SignParams {
pub struct SignedPayloadStream<S>
where
S: Stream<Item = Result<Bytes, Error>>,
{
#[pin]
stream: S,
buf: bytes::BytesMut,
datetime: DateTime<Utc>, datetime: DateTime<Utc>,
scope: String, scope: String,
signing_hmac: HmacSha256, signing_hmac: HmacSha256,
previous_signature: Hash, previous_signature: Hash,
} }
impl<S> SignedPayloadStream<S> #[pin_project::pin_project]
pub struct StreamingPayloadStream<S>
where where
S: Stream<Item = Result<Bytes, Error>>, S: Stream<Item = Result<Bytes, Error>>,
{ {
pub fn new( #[pin]
stream: S, stream: S,
signing_hmac: HmacSha256, buf: bytes::BytesMut,
datetime: DateTime<Utc>, signing: Option<SignParams>,
scope: &str, has_trailer: bool,
seed_signature: Hash, done: bool,
) -> Self { }
impl<S> StreamingPayloadStream<S>
where
S: Stream<Item = Result<Bytes, Error>>,
{
fn new(stream: S, signing: Option<SignParams>, has_trailer: bool) -> Self {
Self { Self {
stream, stream,
buf: bytes::BytesMut::new(), buf: bytes::BytesMut::new(),
datetime, signing,
scope: scope.into(), has_trailer,
signing_hmac, done: false,
previous_signature: seed_signature,
} }
} }
fn parse_next(input: &[u8]) -> nom::IResult<&[u8], SignedPayload, SignedPayloadStreamError> { fn parse_next(
input: &[u8],
is_signed: bool,
has_trailer: bool,
) -> nom::IResult<&[u8], StreamingPayloadChunk, StreamingPayloadError> {
use nom::bytes::streaming::{tag, take}; use nom::bytes::streaming::{tag, take};
macro_rules! try_parse { macro_rules! try_parse {
@@ -238,17 +413,30 @@ where
}; };
} }
let (input, header) = try_parse!(payload::Header::parse(input)); let (input, header) = if is_signed {
try_parse!(payload::ChunkHeader::parse_signed(input))
} else {
try_parse!(payload::ChunkHeader::parse_unsigned(input))
};
// 0-sized chunk is the last // 0-sized chunk is the last
if header.size == 0 { if header.size == 0 {
return Ok(( if has_trailer {
input, let (input, trailer) = if is_signed {
SignedPayload { try_parse!(payload::TrailerChunk::parse_signed(input))
header, } else {
data: Bytes::new(), try_parse!(payload::TrailerChunk::parse_unsigned(input))
}, };
)); return Ok((input, StreamingPayloadChunk::Trailer(trailer)));
} else {
return Ok((
input,
StreamingPayloadChunk::Chunk {
header,
data: Bytes::new(),
},
));
}
} }
let (input, data) = try_parse!(take::<_, _, nom::error::Error<_>>(header.size)(input)); let (input, data) = try_parse!(take::<_, _, nom::error::Error<_>>(header.size)(input));
@@ -256,15 +444,15 @@ where
let data = Bytes::from(data.to_vec()); let data = Bytes::from(data.to_vec());
Ok((input, SignedPayload { header, data })) Ok((input, StreamingPayloadChunk::Chunk { header, data }))
} }
} }
impl<S> Stream for SignedPayloadStream<S> impl<S> Stream for StreamingPayloadStream<S>
where where
S: Stream<Item = Result<Bytes, Error>> + Unpin, S: Stream<Item = Result<Bytes, Error>> + Unpin,
{ {
type Item = Result<Bytes, SignedPayloadStreamError>; type Item = Result<Frame<Bytes>, StreamingPayloadError>;
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
@@ -274,56 +462,105 @@ where
let mut this = self.project(); let mut this = self.project();
if *this.done {
return Poll::Ready(None);
}
loop { loop {
let (input, payload) = match Self::parse_next(this.buf) { let (input, payload) =
Ok(res) => res, match Self::parse_next(this.buf, this.signing.is_some(), *this.has_trailer) {
Err(nom::Err::Incomplete(_)) => { Ok(res) => res,
match futures::ready!(this.stream.as_mut().poll_next(cx)) { Err(nom::Err::Incomplete(_)) => {
Some(Ok(bytes)) => { match futures::ready!(this.stream.as_mut().poll_next(cx)) {
this.buf.extend(bytes); Some(Ok(bytes)) => {
continue; this.buf.extend(bytes);
} continue;
Some(Err(e)) => { }
return Poll::Ready(Some(Err(SignedPayloadStreamError::Stream(e)))) Some(Err(e)) => {
} return Poll::Ready(Some(Err(StreamingPayloadError::Stream(e))))
None => { }
return Poll::Ready(Some(Err(SignedPayloadStreamError::message( None => {
"Unexpected EOF", return Poll::Ready(Some(Err(StreamingPayloadError::message(
)))); "Unexpected EOF",
))));
}
} }
} }
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
return Poll::Ready(Some(Err(e)))
}
};
match payload {
StreamingPayloadChunk::Chunk { data, header } => {
if let Some(signing) = this.signing.as_mut() {
let data_sha256sum = sha256sum(&data);
let expected_signature = compute_streaming_payload_signature(
&signing.signing_hmac,
signing.datetime,
&signing.scope,
signing.previous_signature,
data_sha256sum,
)?;
if header.signature.unwrap() != expected_signature {
return Poll::Ready(Some(Err(StreamingPayloadError::InvalidSignature)));
}
signing.previous_signature = header.signature.unwrap();
}
*this.buf = input.into();
// 0-sized chunk is the last
if data.is_empty() {
// if there was a trailer, it would have been returned by the parser
assert!(!*this.has_trailer);
*this.done = true;
return Poll::Ready(None);
}
return Poll::Ready(Some(Ok(Frame::data(data))));
} }
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => { StreamingPayloadChunk::Trailer(trailer) => {
return Poll::Ready(Some(Err(e))) trace!(
"In StreamingPayloadStream::poll_next: got trailer {:?}",
trailer
);
if let Some(signing) = this.signing.as_mut() {
let data = [
trailer.header_name.as_ref(),
&b":"[..],
trailer.header_value.as_ref(),
&b"\n"[..],
]
.concat();
let trailer_sha256sum = sha256sum(&data);
let expected_signature = compute_streaming_trailer_signature(
&signing.signing_hmac,
signing.datetime,
&signing.scope,
signing.previous_signature,
trailer_sha256sum,
)?;
if trailer.signature.unwrap() != expected_signature {
return Poll::Ready(Some(Err(StreamingPayloadError::InvalidSignature)));
}
}
*this.buf = input.into();
*this.done = true;
let mut trailers_map = HeaderMap::new();
trailers_map.insert(trailer.header_name, trailer.header_value);
return Poll::Ready(Some(Ok(Frame::trailers(trailers_map))));
} }
};
// 0-sized chunk is the last
if payload.data.is_empty() {
return Poll::Ready(None);
} }
let data_sha256sum = sha256sum(&payload.data);
let expected_signature = compute_streaming_payload_signature(
this.signing_hmac,
*this.datetime,
this.scope,
*this.previous_signature,
data_sha256sum,
)
.map_err(|e| {
SignedPayloadStreamError::Message(format!("Could not build signature: {}", e))
})?;
if payload.header.signature != expected_signature {
return Poll::Ready(Some(Err(SignedPayloadStreamError::InvalidSignature)));
}
*this.buf = input.into();
*this.previous_signature = payload.header.signature;
return Poll::Ready(Some(Ok(payload.data)));
} }
} }
@@ -336,7 +573,7 @@ where
mod tests { mod tests {
use futures::prelude::*; use futures::prelude::*;
use super::{SignedPayloadStream, SignedPayloadStreamError}; use super::{SignParams, StreamingPayloadError, StreamingPayloadStream};
#[tokio::test] #[tokio::test]
async fn test_interrupted_signed_payload_stream() { async fn test_interrupted_signed_payload_stream() {
@@ -358,12 +595,20 @@ mod tests {
let seed_signature = Hash::default(); let seed_signature = Hash::default();
let mut stream = let mut stream = StreamingPayloadStream::new(
SignedPayloadStream::new(body, signing_hmac, datetime, &scope, seed_signature); body,
Some(SignParams {
signing_hmac,
datetime,
scope,
previous_signature: seed_signature,
}),
false,
);
assert!(stream.try_next().await.is_err()); assert!(stream.try_next().await.is_err());
match stream.try_next().await { match stream.try_next().await {
Err(SignedPayloadStreamError::Message(msg)) if msg == "Unexpected EOF" => {} Err(StreamingPayloadError::Message(msg)) if msg == "Unexpected EOF" => {}
item => panic!( item => panic!(
"Unexpected result, expected early EOF error, got {:?}", "Unexpected result, expected early EOF error, got {:?}",
item item
+2 -2
View File
@@ -1,12 +1,12 @@
[package] [package]
name = "garage_api_k2v" name = "garage_api_k2v"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
description = "K2V API server crate for the Garage object store" description = "K2V API server crate for the Garage object store"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage" repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
readme = "../../README.md" readme = "../../../README.md"
[lib] [lib]
path = "lib.rs" path = "lib.rs"
+3 -1
View File
@@ -81,7 +81,9 @@ impl ApiHandler for K2VApiServer {
return Ok(options_res.map(|_empty_body: EmptyBody| empty_body())); return Ok(options_res.map(|_empty_body: EmptyBody| empty_body()));
} }
let (req, api_key, _content_sha256) = verify_request(&garage, req, "k2v").await?; let verified_request = verify_request(&garage, req, "k2v").await?;
let req = verified_request.request;
let api_key = verified_request.access_key;
let bucket_id = garage let bucket_id = garage
.bucket_helper() .bucket_helper()
+4 -4
View File
@@ -20,7 +20,7 @@ pub async fn handle_insert_batch(
let ReqCtx { let ReqCtx {
garage, bucket_id, .. garage, bucket_id, ..
} = &ctx; } = &ctx;
let items = parse_json_body::<Vec<InsertBatchItem>, _, Error>(req).await?; let items = req.into_body().json::<Vec<InsertBatchItem>>().await?;
let mut items2 = vec![]; let mut items2 = vec![];
for it in items { for it in items {
@@ -47,7 +47,7 @@ pub async fn handle_read_batch(
ctx: ReqCtx, ctx: ReqCtx,
req: Request<ReqBody>, req: Request<ReqBody>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let queries = parse_json_body::<Vec<ReadBatchQuery>, _, Error>(req).await?; let queries = req.into_body().json::<Vec<ReadBatchQuery>>().await?;
let resp_results = futures::future::join_all( let resp_results = futures::future::join_all(
queries queries
@@ -141,7 +141,7 @@ pub async fn handle_delete_batch(
ctx: ReqCtx, ctx: ReqCtx,
req: Request<ReqBody>, req: Request<ReqBody>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let queries = parse_json_body::<Vec<DeleteBatchQuery>, _, Error>(req).await?; let queries = req.into_body().json::<Vec<DeleteBatchQuery>>().await?;
let resp_results = futures::future::join_all( let resp_results = futures::future::join_all(
queries queries
@@ -262,7 +262,7 @@ pub(crate) async fn handle_poll_range(
} = ctx; } = ctx;
use garage_model::k2v::sub::PollRange; use garage_model::k2v::sub::PollRange;
let query = parse_json_body::<PollRangeQuery, _, Error>(req).await?; let query = req.into_body().json::<PollRangeQuery>().await?;
let timeout_msec = query.timeout.unwrap_or(300).clamp(1, 600) * 1000; let timeout_msec = query.timeout.unwrap_or(300).clamp(1, 600) * 1000;
+7
View File
@@ -23,6 +23,10 @@ pub enum Error {
#[error(display = "Authorization header malformed, unexpected scope: {}", _0)] #[error(display = "Authorization header malformed, unexpected scope: {}", _0)]
AuthorizationHeaderMalformed(String), AuthorizationHeaderMalformed(String),
/// The provided digest (checksum) value was invalid
#[error(display = "Invalid digest: {}", _0)]
InvalidDigest(String),
/// The object requested don't exists /// The object requested don't exists
#[error(display = "Key not found")] #[error(display = "Key not found")]
NoSuchKey, NoSuchKey,
@@ -54,6 +58,7 @@ impl From<SignatureError> for Error {
Self::AuthorizationHeaderMalformed(c) Self::AuthorizationHeaderMalformed(c)
} }
SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i), SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i),
SignatureError::InvalidDigest(d) => Self::InvalidDigest(d),
} }
} }
} }
@@ -71,6 +76,7 @@ impl Error {
Error::InvalidBase64(_) => "InvalidBase64", Error::InvalidBase64(_) => "InvalidBase64",
Error::InvalidUtf8Str(_) => "InvalidUtf8String", Error::InvalidUtf8Str(_) => "InvalidUtf8String",
Error::InvalidCausalityToken => "CausalityToken", Error::InvalidCausalityToken => "CausalityToken",
Error::InvalidDigest(_) => "InvalidDigest",
} }
} }
} }
@@ -85,6 +91,7 @@ impl ApiError for Error {
Error::AuthorizationHeaderMalformed(_) Error::AuthorizationHeaderMalformed(_)
| Error::InvalidBase64(_) | Error::InvalidBase64(_)
| Error::InvalidUtf8Str(_) | Error::InvalidUtf8Str(_)
| Error::InvalidDigest(_)
| Error::InvalidCausalityToken => StatusCode::BAD_REQUEST, | Error::InvalidCausalityToken => StatusCode::BAD_REQUEST,
} }
} }
+1 -3
View File
@@ -144,9 +144,7 @@ pub async fn handle_insert_item(
.map(parse_causality_token) .map(parse_causality_token)
.transpose()?; .transpose()?;
let body = http_body_util::BodyExt::collect(req.into_body()) let body = req.into_body().collect().await?;
.await?
.to_bytes();
let value = DvvsValue::Value(body.to_vec()); let value = DvvsValue::Value(body.to_vec());
+2 -2
View File
@@ -1,12 +1,12 @@
[package] [package]
name = "garage_api_s3" name = "garage_api_s3"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
description = "S3 API server crate for the Garage object store" description = "S3 API server crate for the Garage object store"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage" repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
readme = "../../README.md" readme = "../../../README.md"
[lib] [lib]
path = "lib.rs" path = "lib.rs"
+13 -20
View File
@@ -121,7 +121,9 @@ impl ApiHandler for S3ApiServer {
return Ok(options_res.map(|_empty_body: EmptyBody| empty_body())); return Ok(options_res.map(|_empty_body: EmptyBody| empty_body()));
} }
let (req, api_key, content_sha256) = verify_request(&garage, req, "s3").await?; let verified_request = verify_request(&garage, req, "s3").await?;
let req = verified_request.request;
let api_key = verified_request.access_key;
let bucket_name = match bucket_name { let bucket_name = match bucket_name {
None => { None => {
@@ -134,14 +136,7 @@ impl ApiHandler for S3ApiServer {
// Special code path for CreateBucket API endpoint // Special code path for CreateBucket API endpoint
if let Endpoint::CreateBucket {} = endpoint { if let Endpoint::CreateBucket {} = endpoint {
return handle_create_bucket( return handle_create_bucket(&garage, req, &api_key.key_id, bucket_name).await;
&garage,
req,
content_sha256,
&api_key.key_id,
bucket_name,
)
.await;
} }
let bucket_id = garage let bucket_id = garage
@@ -179,7 +174,7 @@ impl ApiHandler for S3ApiServer {
let resp = match endpoint { let resp = match endpoint {
Endpoint::HeadObject { Endpoint::HeadObject {
key, part_number, .. key, part_number, ..
} => handle_head(ctx, &req, &key, part_number).await, } => handle_head(ctx, &req.map(|_| ()), &key, part_number).await,
Endpoint::GetObject { Endpoint::GetObject {
key, key,
part_number, part_number,
@@ -199,20 +194,20 @@ impl ApiHandler for S3ApiServer {
response_content_type, response_content_type,
response_expires, response_expires,
}; };
handle_get(ctx, &req, &key, part_number, overrides).await handle_get(ctx, &req.map(|_| ()), &key, part_number, overrides).await
} }
Endpoint::UploadPart { Endpoint::UploadPart {
key, key,
part_number, part_number,
upload_id, upload_id,
} => handle_put_part(ctx, req, &key, part_number, &upload_id, content_sha256).await, } => handle_put_part(ctx, req, &key, part_number, &upload_id).await,
Endpoint::CopyObject { key } => handle_copy(ctx, &req, &key).await, Endpoint::CopyObject { key } => handle_copy(ctx, &req, &key).await,
Endpoint::UploadPartCopy { Endpoint::UploadPartCopy {
key, key,
part_number, part_number,
upload_id, upload_id,
} => handle_upload_part_copy(ctx, &req, &key, part_number, &upload_id).await, } => handle_upload_part_copy(ctx, &req, &key, part_number, &upload_id).await,
Endpoint::PutObject { key } => handle_put(ctx, req, &key, content_sha256).await, Endpoint::PutObject { key } => handle_put(ctx, req, &key).await,
Endpoint::AbortMultipartUpload { key, upload_id } => { Endpoint::AbortMultipartUpload { key, upload_id } => {
handle_abort_multipart_upload(ctx, &key, &upload_id).await handle_abort_multipart_upload(ctx, &key, &upload_id).await
} }
@@ -221,7 +216,7 @@ impl ApiHandler for S3ApiServer {
handle_create_multipart_upload(ctx, &req, &key).await handle_create_multipart_upload(ctx, &req, &key).await
} }
Endpoint::CompleteMultipartUpload { key, upload_id } => { Endpoint::CompleteMultipartUpload { key, upload_id } => {
handle_complete_multipart_upload(ctx, req, &key, &upload_id, content_sha256).await handle_complete_multipart_upload(ctx, req, &key, &upload_id).await
} }
Endpoint::CreateBucket {} => unreachable!(), Endpoint::CreateBucket {} => unreachable!(),
Endpoint::HeadBucket {} => { Endpoint::HeadBucket {} => {
@@ -324,17 +319,15 @@ impl ApiHandler for S3ApiServer {
}; };
handle_list_parts(ctx, req, &query).await handle_list_parts(ctx, req, &query).await
} }
Endpoint::DeleteObjects {} => handle_delete_objects(ctx, req, content_sha256).await, Endpoint::DeleteObjects {} => handle_delete_objects(ctx, req).await,
Endpoint::GetBucketWebsite {} => handle_get_website(ctx).await, Endpoint::GetBucketWebsite {} => handle_get_website(ctx).await,
Endpoint::PutBucketWebsite {} => handle_put_website(ctx, req, content_sha256).await, Endpoint::PutBucketWebsite {} => handle_put_website(ctx, req).await,
Endpoint::DeleteBucketWebsite {} => handle_delete_website(ctx).await, Endpoint::DeleteBucketWebsite {} => handle_delete_website(ctx).await,
Endpoint::GetBucketCors {} => handle_get_cors(ctx).await, Endpoint::GetBucketCors {} => handle_get_cors(ctx).await,
Endpoint::PutBucketCors {} => handle_put_cors(ctx, req, content_sha256).await, Endpoint::PutBucketCors {} => handle_put_cors(ctx, req).await,
Endpoint::DeleteBucketCors {} => handle_delete_cors(ctx).await, Endpoint::DeleteBucketCors {} => handle_delete_cors(ctx).await,
Endpoint::GetBucketLifecycleConfiguration {} => handle_get_lifecycle(ctx).await, Endpoint::GetBucketLifecycleConfiguration {} => handle_get_lifecycle(ctx).await,
Endpoint::PutBucketLifecycleConfiguration {} => { Endpoint::PutBucketLifecycleConfiguration {} => handle_put_lifecycle(ctx, req).await,
handle_put_lifecycle(ctx, req, content_sha256).await
}
Endpoint::DeleteBucketLifecycle {} => handle_delete_lifecycle(ctx).await, Endpoint::DeleteBucketLifecycle {} => handle_delete_lifecycle(ctx).await,
endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())), endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())),
}; };
+1 -9
View File
@@ -1,6 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use http_body_util::BodyExt;
use hyper::{Request, Response, StatusCode}; use hyper::{Request, Response, StatusCode};
use garage_model::bucket_alias_table::*; use garage_model::bucket_alias_table::*;
@@ -10,12 +9,10 @@ use garage_model::key_table::Key;
use garage_model::permission::BucketKeyPerm; use garage_model::permission::BucketKeyPerm;
use garage_table::util::*; use garage_table::util::*;
use garage_util::crdt::*; use garage_util::crdt::*;
use garage_util::data::*;
use garage_util::time::*; use garage_util::time::*;
use garage_api_common::common_error::CommonError; use garage_api_common::common_error::CommonError;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::verify_signed_content;
use crate::api_server::{ReqBody, ResBody}; use crate::api_server::{ReqBody, ResBody};
use crate::error::*; use crate::error::*;
@@ -122,15 +119,10 @@ pub async fn handle_list_buckets(
pub async fn handle_create_bucket( pub async fn handle_create_bucket(
garage: &Garage, garage: &Garage,
req: Request<ReqBody>, req: Request<ReqBody>,
content_sha256: Option<Hash>,
api_key_id: &String, api_key_id: &String,
bucket_name: String, bucket_name: String,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let body = BodyExt::collect(req.into_body()).await?.to_bytes(); let body = req.into_body().collect().await?;
if let Some(content_sha256) = content_sha256 {
verify_signed_content(content_sha256, &body[..])?;
}
let cmd = let cmd =
parse_create_bucket_xml(&body[..]).ok_or_bad_request("Invalid create bucket XML query")?; parse_create_bucket_xml(&body[..]).ok_or_bad_request("Invalid create bucket XML query")?;
+18 -100
View File
@@ -1,9 +1,9 @@
use std::pin::Pin; use std::pin::Pin;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use futures::{stream, stream::Stream, StreamExt, TryStreamExt}; use futures::{stream, stream::Stream, StreamExt, TryStreamExt};
use bytes::Bytes; use bytes::Bytes;
use http::header::HeaderName;
use hyper::{Request, Response}; use hyper::{Request, Response};
use serde::Serialize; use serde::Serialize;
@@ -21,16 +21,25 @@ use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*; use garage_model::s3::version_table::*;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::checksum::*;
use crate::api_server::{ReqBody, ResBody}; use crate::api_server::{ReqBody, ResBody};
use crate::checksum::*;
use crate::encryption::EncryptionParams; use crate::encryption::EncryptionParams;
use crate::error::*; use crate::error::*;
use crate::get::full_object_byte_stream; use crate::get::{full_object_byte_stream, PreconditionHeaders};
use crate::multipart; 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}; use crate::xml::{self as s3_xml, xmlns_tag};
pub const X_AMZ_COPY_SOURCE_IF_MATCH: HeaderName =
HeaderName::from_static("x-amz-copy-source-if-match");
pub const X_AMZ_COPY_SOURCE_IF_NONE_MATCH: HeaderName =
HeaderName::from_static("x-amz-copy-source-if-none-match");
pub const X_AMZ_COPY_SOURCE_IF_MODIFIED_SINCE: HeaderName =
HeaderName::from_static("x-amz-copy-source-if-modified-since");
pub const X_AMZ_COPY_SOURCE_IF_UNMODIFIED_SINCE: HeaderName =
HeaderName::from_static("x-amz-copy-source-if-unmodified-since");
// -------- CopyObject --------- // -------- CopyObject ---------
pub async fn handle_copy( pub async fn handle_copy(
@@ -38,7 +47,7 @@ pub async fn handle_copy(
req: &Request<ReqBody>, req: &Request<ReqBody>,
dest_key: &str, dest_key: &str,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let copy_precondition = CopyPreconditionHeaders::parse(req)?; let copy_precondition = PreconditionHeaders::parse_copy_source(req)?;
let checksum_algorithm = request_checksum_algorithm(req.headers())?; let checksum_algorithm = request_checksum_algorithm(req.headers())?;
@@ -48,7 +57,7 @@ pub async fn handle_copy(
extract_source_info(&source_object)?; extract_source_info(&source_object)?;
// Check precondition, e.g. x-amz-copy-source-if-match // Check precondition, e.g. x-amz-copy-source-if-match
copy_precondition.check(source_version, &source_version_meta.etag)?; copy_precondition.check_copy_source(source_version, &source_version_meta.etag)?;
// Determine encryption parameters // Determine encryption parameters
let (source_encryption, source_object_meta_inner) = let (source_encryption, source_object_meta_inner) =
@@ -73,7 +82,7 @@ pub async fn handle_copy(
let dest_object_meta = ObjectVersionMetaInner { let dest_object_meta = ObjectVersionMetaInner {
headers: match req.headers().get("x-amz-metadata-directive") { headers: match req.headers().get("x-amz-metadata-directive") {
Some(v) if v == hyper::header::HeaderValue::from_static("REPLACE") => { 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, _ => source_object_meta_inner.into_owned().headers,
}, },
@@ -335,7 +344,7 @@ pub async fn handle_upload_part_copy(
part_number: u64, part_number: u64,
upload_id: &str, upload_id: &str,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let copy_precondition = CopyPreconditionHeaders::parse(req)?; let copy_precondition = PreconditionHeaders::parse_copy_source(req)?;
let dest_upload_id = multipart::decode_upload_id(upload_id)?; let dest_upload_id = multipart::decode_upload_id(upload_id)?;
@@ -351,7 +360,7 @@ pub async fn handle_upload_part_copy(
extract_source_info(&source_object)?; extract_source_info(&source_object)?;
// Check precondition on source, e.g. x-amz-copy-source-if-match // Check precondition on source, e.g. x-amz-copy-source-if-match
copy_precondition.check(source_object_version, &source_version_meta.etag)?; copy_precondition.check_copy_source(source_object_version, &source_version_meta.etag)?;
// Determine encryption parameters // Determine encryption parameters
let (source_encryption, _) = EncryptionParams::check_decrypt_for_copy_source( let (source_encryption, _) = EncryptionParams::check_decrypt_for_copy_source(
@@ -703,97 +712,6 @@ fn extract_source_info(
Ok((source_version, source_version_data, source_version_meta)) Ok((source_version, source_version_data, source_version_meta))
} }
struct CopyPreconditionHeaders {
copy_source_if_match: Option<Vec<String>>,
copy_source_if_modified_since: Option<SystemTime>,
copy_source_if_none_match: Option<Vec<String>>,
copy_source_if_unmodified_since: Option<SystemTime>,
}
impl CopyPreconditionHeaders {
fn parse(req: &Request<ReqBody>) -> Result<Self, Error> {
Ok(Self {
copy_source_if_match: req
.headers()
.get("x-amz-copy-source-if-match")
.map(|x| x.to_str())
.transpose()?
.map(|x| {
x.split(',')
.map(|m| m.trim().trim_matches('"').to_string())
.collect::<Vec<_>>()
}),
copy_source_if_modified_since: req
.headers()
.get("x-amz-copy-source-if-modified-since")
.map(|x| x.to_str())
.transpose()?
.map(httpdate::parse_http_date)
.transpose()
.ok_or_bad_request("Invalid date in x-amz-copy-source-if-modified-since")?,
copy_source_if_none_match: req
.headers()
.get("x-amz-copy-source-if-none-match")
.map(|x| x.to_str())
.transpose()?
.map(|x| {
x.split(',')
.map(|m| m.trim().trim_matches('"').to_string())
.collect::<Vec<_>>()
}),
copy_source_if_unmodified_since: req
.headers()
.get("x-amz-copy-source-if-unmodified-since")
.map(|x| x.to_str())
.transpose()?
.map(httpdate::parse_http_date)
.transpose()
.ok_or_bad_request("Invalid date in x-amz-copy-source-if-unmodified-since")?,
})
}
fn check(&self, v: &ObjectVersion, etag: &str) -> Result<(), Error> {
let v_date = UNIX_EPOCH + Duration::from_millis(v.timestamp);
let ok = match (
&self.copy_source_if_match,
&self.copy_source_if_unmodified_since,
&self.copy_source_if_none_match,
&self.copy_source_if_modified_since,
) {
// TODO I'm not sure all of the conditions are evaluated correctly here
// If we have both if-match and if-unmodified-since,
// basically we don't care about if-unmodified-since,
// because in the spec it says that if if-match evaluates to
// true but if-unmodified-since evaluates to false,
// the copy is still done.
(Some(im), _, None, None) => im.iter().any(|x| x == etag || x == "*"),
(None, Some(ius), None, None) => v_date <= *ius,
// If we have both if-none-match and if-modified-since,
// then both of the two conditions must evaluate to true
(None, None, Some(inm), Some(ims)) => {
!inm.iter().any(|x| x == etag || x == "*") && v_date > *ims
}
(None, None, Some(inm), None) => !inm.iter().any(|x| x == etag || x == "*"),
(None, None, None, Some(ims)) => v_date > *ims,
(None, None, None, None) => true,
_ => {
return Err(Error::bad_request(
"Invalid combination of x-amz-copy-source-if-xxxxx headers",
))
}
};
if ok {
Ok(())
} else {
Err(Error::PreconditionFailed)
}
}
}
type BlockStreamItemOk = (Bytes, Option<Hash>); type BlockStreamItemOk = (Bytes, Option<Hash>);
type BlockStreamItem = Result<BlockStreamItemOk, garage_util::error::Error>; type BlockStreamItem = Result<BlockStreamItemOk, garage_util::error::Error>;
+1 -10
View File
@@ -2,15 +2,11 @@ use quick_xml::de::from_reader;
use hyper::{header::HeaderName, Method, Request, Response, StatusCode}; use hyper::{header::HeaderName, Method, Request, Response, StatusCode};
use http_body_util::BodyExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use garage_model::bucket_table::{Bucket, CorsRule as GarageCorsRule}; use garage_model::bucket_table::{Bucket, CorsRule as GarageCorsRule};
use garage_util::data::*;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::verify_signed_content;
use crate::api_server::{ReqBody, ResBody}; use crate::api_server::{ReqBody, ResBody};
use crate::error::*; use crate::error::*;
@@ -59,7 +55,6 @@ pub async fn handle_delete_cors(ctx: ReqCtx) -> Result<Response<ResBody>, Error>
pub async fn handle_put_cors( pub async fn handle_put_cors(
ctx: ReqCtx, ctx: ReqCtx,
req: Request<ReqBody>, req: Request<ReqBody>,
content_sha256: Option<Hash>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let ReqCtx { let ReqCtx {
garage, garage,
@@ -68,11 +63,7 @@ pub async fn handle_put_cors(
.. ..
} = ctx; } = ctx;
let body = BodyExt::collect(req.into_body()).await?.to_bytes(); let body = req.into_body().collect().await?;
if let Some(content_sha256) = content_sha256 {
verify_signed_content(content_sha256, &body[..])?;
}
let conf: CorsConfiguration = from_reader(&body as &[u8])?; let conf: CorsConfiguration = from_reader(&body as &[u8])?;
conf.validate()?; conf.validate()?;
+1 -8
View File
@@ -1,4 +1,3 @@
use http_body_util::BodyExt;
use hyper::{Request, Response, StatusCode}; use hyper::{Request, Response, StatusCode};
use garage_util::data::*; use garage_util::data::*;
@@ -6,7 +5,6 @@ use garage_util::data::*;
use garage_model::s3::object_table::*; use garage_model::s3::object_table::*;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::verify_signed_content;
use crate::api_server::{ReqBody, ResBody}; use crate::api_server::{ReqBody, ResBody};
use crate::error::*; use crate::error::*;
@@ -68,13 +66,8 @@ pub async fn handle_delete(ctx: ReqCtx, key: &str) -> Result<Response<ResBody>,
pub async fn handle_delete_objects( pub async fn handle_delete_objects(
ctx: ReqCtx, ctx: ReqCtx,
req: Request<ReqBody>, req: Request<ReqBody>,
content_sha256: Option<Hash>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let body = BodyExt::collect(req.into_body()).await?.to_bytes(); let body = req.into_body().collect().await?;
if let Some(content_sha256) = content_sha256 {
verify_signed_content(content_sha256, &body[..])?;
}
let cmd_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?; let cmd_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?;
let cmd = parse_delete_objects_xml(&cmd_xml).ok_or_bad_request("Invalid delete XML query")?; let cmd = parse_delete_objects_xml(&cmd_xml).ok_or_bad_request("Invalid delete XML query")?;
+1 -1
View File
@@ -29,8 +29,8 @@ use garage_model::garage::Garage;
use garage_model::s3::object_table::{ObjectVersionEncryption, ObjectVersionMetaInner}; use garage_model::s3::object_table::{ObjectVersionEncryption, ObjectVersionMetaInner};
use garage_api_common::common_error::*; use garage_api_common::common_error::*;
use garage_api_common::signature::checksum::Md5Checksum;
use crate::checksum::Md5Checksum;
use crate::error::Error; use crate::error::Error;
const X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM: HeaderName = const X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM: HeaderName =
+2 -1
View File
@@ -80,7 +80,7 @@ pub enum Error {
#[error(display = "Invalid encryption algorithm: {:?}, should be AES256", _0)] #[error(display = "Invalid encryption algorithm: {:?}, should be AES256", _0)]
InvalidEncryptionAlgorithm(String), InvalidEncryptionAlgorithm(String),
/// The client sent invalid XML data /// The provided digest (checksum) value was invalid
#[error(display = "Invalid digest: {}", _0)] #[error(display = "Invalid digest: {}", _0)]
InvalidDigest(String), InvalidDigest(String),
@@ -119,6 +119,7 @@ impl From<SignatureError> for Error {
Self::AuthorizationHeaderMalformed(c) Self::AuthorizationHeaderMalformed(c)
} }
SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i), SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i),
SignatureError::InvalidDigest(d) => Self::InvalidDigest(d),
} }
} }
} }
+152 -48
View File
@@ -2,17 +2,17 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::convert::TryInto; use std::convert::TryInto;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use bytes::Bytes; use bytes::Bytes;
use futures::future; use futures::future;
use futures::stream::{self, Stream, StreamExt}; use futures::stream::{self, Stream, StreamExt};
use http::header::{ use http::header::{
ACCEPT_RANGES, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LANGUAGE, HeaderMap, HeaderName, ACCEPT_RANGES, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING,
CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, EXPIRES, IF_MODIFIED_SINCE, IF_NONE_MATCH, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, EXPIRES, IF_MATCH,
LAST_MODIFIED, RANGE, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_UNMODIFIED_SINCE, LAST_MODIFIED, RANGE,
}; };
use hyper::{body::Body, Request, Response, StatusCode}; use hyper::{Request, Response, StatusCode};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use garage_net::stream::ByteStream; use garage_net::stream::ByteStream;
@@ -26,13 +26,14 @@ use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*; use garage_model::s3::version_table::*;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::checksum::{add_checksum_response_headers, X_AMZ_CHECKSUM_MODE};
use crate::api_server::ResBody; use crate::api_server::ResBody;
use crate::checksum::{add_checksum_response_headers, X_AMZ_CHECKSUM_MODE}; use crate::copy::*;
use crate::encryption::EncryptionParams; use crate::encryption::EncryptionParams;
use crate::error::*; use crate::error::*;
const X_AMZ_MP_PARTS_COUNT: &str = "x-amz-mp-parts-count"; const X_AMZ_MP_PARTS_COUNT: HeaderName = HeaderName::from_static("x-amz-mp-parts-count");
#[derive(Default)] #[derive(Default)]
pub struct GetObjectOverrides { pub struct GetObjectOverrides {
@@ -115,49 +116,29 @@ fn getobject_override_headers(
Ok(()) Ok(())
} }
fn try_answer_cached( fn handle_http_precondition(
version: &ObjectVersion, version: &ObjectVersion,
version_meta: &ObjectVersionMeta, version_meta: &ObjectVersionMeta,
req: &Request<impl Body>, req: &Request<()>,
) -> Option<Response<ResBody>> { ) -> Result<Option<Response<ResBody>>, Error> {
// <trinity> It is possible, and is even usually the case, [that both If-None-Match and let precondition_headers = PreconditionHeaders::parse(req)?;
// If-Modified-Since] are present in a request. In this situation If-None-Match takes
// precedence and If-Modified-Since is ignored (as per 6.Precedence from rfc7232). The rational
// being that etag based matching is more accurate, it has no issue with sub-second precision
// for instance (in case of very fast updates)
let cached = if let Some(none_match) = req.headers().get(IF_NONE_MATCH) {
let none_match = none_match.to_str().ok()?;
let expected = format!("\"{}\"", version_meta.etag);
let found = none_match
.split(',')
.map(str::trim)
.any(|etag| etag == expected || etag == "\"*\"");
found
} else if let Some(modified_since) = req.headers().get(IF_MODIFIED_SINCE) {
let modified_since = modified_since.to_str().ok()?;
let client_date = httpdate::parse_http_date(modified_since).ok()?;
let server_date = UNIX_EPOCH + Duration::from_millis(version.timestamp);
client_date >= server_date
} else {
false
};
if cached { if let Some(status_code) = precondition_headers.check(&version, &version_meta.etag)? {
Some( Ok(Some(
Response::builder() Response::builder()
.status(StatusCode::NOT_MODIFIED) .status(status_code)
.body(empty_body()) .body(empty_body())
.unwrap(), .unwrap(),
) ))
} else { } else {
None Ok(None)
} }
} }
/// Handle HEAD request /// Handle HEAD request
pub async fn handle_head( pub async fn handle_head(
ctx: ReqCtx, ctx: ReqCtx,
req: &Request<impl Body>, req: &Request<()>,
key: &str, key: &str,
part_number: Option<u64>, part_number: Option<u64>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
@@ -167,7 +148,7 @@ pub async fn handle_head(
/// Handle HEAD request for website /// Handle HEAD request for website
pub async fn handle_head_without_ctx( pub async fn handle_head_without_ctx(
garage: Arc<Garage>, garage: Arc<Garage>,
req: &Request<impl Body>, req: &Request<()>,
bucket_id: Uuid, bucket_id: Uuid,
key: &str, key: &str,
part_number: Option<u64>, part_number: Option<u64>,
@@ -196,8 +177,8 @@ pub async fn handle_head_without_ctx(
_ => unreachable!(), _ => unreachable!(),
}; };
if let Some(cached) = try_answer_cached(object_version, version_meta, req) { if let Some(res) = handle_http_precondition(object_version, version_meta, req)? {
return Ok(cached); return Ok(res);
} }
let (encryption, headers) = let (encryption, headers) =
@@ -278,7 +259,7 @@ 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<()>,
key: &str, key: &str,
part_number: Option<u64>, part_number: Option<u64>,
overrides: GetObjectOverrides, overrides: GetObjectOverrides,
@@ -289,7 +270,7 @@ pub async fn handle_get(
/// Handle GET request /// Handle GET request
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<()>,
bucket_id: Uuid, bucket_id: Uuid,
key: &str, key: &str,
part_number: Option<u64>, part_number: Option<u64>,
@@ -318,8 +299,8 @@ pub async fn handle_get_without_ctx(
ObjectVersionData::FirstBlock(meta, _) => meta, ObjectVersionData::FirstBlock(meta, _) => meta,
}; };
if let Some(cached) = try_answer_cached(last_v, last_v_meta, req) { if let Some(res) = handle_http_precondition(last_v, last_v_meta, req)? {
return Ok(cached); return Ok(res);
} }
let (enc, headers) = let (enc, headers) =
@@ -340,7 +321,12 @@ pub async fn handle_get_without_ctx(
enc, enc,
&headers, &headers,
pn, pn,
checksum_mode, ChecksumMode {
// TODO: for multipart uploads, checksums of each part should be stored
// so that we can return the corresponding checksum here
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html
enabled: false,
},
) )
.await .await
} }
@@ -354,7 +340,12 @@ pub async fn handle_get_without_ctx(
&headers, &headers,
range.start, range.start,
range.start + range.length, range.start + range.length,
checksum_mode, ChecksumMode {
// TODO: for range queries that align with part boundaries,
// we should return the saved checksum of the part
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html
enabled: false,
},
) )
.await .await
} }
@@ -577,7 +568,7 @@ async fn handle_get_part(
} }
fn parse_range_header( fn parse_range_header(
req: &Request<impl Body>, req: &Request<()>,
total_size: u64, total_size: u64,
) -> Result<Option<http_range::HttpRange>, Error> { ) -> Result<Option<http_range::HttpRange>, Error> {
let range = match req.headers().get(RANGE) { let range = match req.headers().get(RANGE) {
@@ -618,7 +609,7 @@ struct ChecksumMode {
enabled: bool, enabled: bool,
} }
fn checksum_mode(req: &Request<impl Body>) -> ChecksumMode { fn checksum_mode(req: &Request<()>) -> ChecksumMode {
ChecksumMode { ChecksumMode {
enabled: req enabled: req
.headers() .headers()
@@ -751,3 +742,116 @@ fn std_error_from_read_error<E: std::fmt::Display>(e: E) -> std::io::Error {
format!("Error while reading object data: {}", e), format!("Error while reading object data: {}", e),
) )
} }
// ----
pub struct PreconditionHeaders {
if_match: Option<Vec<String>>,
if_modified_since: Option<SystemTime>,
if_none_match: Option<Vec<String>>,
if_unmodified_since: Option<SystemTime>,
}
impl PreconditionHeaders {
fn parse<B>(req: &Request<B>) -> Result<Self, Error> {
Self::parse_with(
req.headers(),
&IF_MATCH,
&IF_NONE_MATCH,
&IF_MODIFIED_SINCE,
&IF_UNMODIFIED_SINCE,
)
}
pub(crate) fn parse_copy_source<B>(req: &Request<B>) -> Result<Self, Error> {
Self::parse_with(
req.headers(),
&X_AMZ_COPY_SOURCE_IF_MATCH,
&X_AMZ_COPY_SOURCE_IF_NONE_MATCH,
&X_AMZ_COPY_SOURCE_IF_MODIFIED_SINCE,
&X_AMZ_COPY_SOURCE_IF_UNMODIFIED_SINCE,
)
}
fn parse_with(
headers: &HeaderMap,
hdr_if_match: &HeaderName,
hdr_if_none_match: &HeaderName,
hdr_if_modified_since: &HeaderName,
hdr_if_unmodified_since: &HeaderName,
) -> Result<Self, Error> {
Ok(Self {
if_match: headers
.get(hdr_if_match)
.map(|x| x.to_str())
.transpose()?
.map(|x| {
x.split(',')
.map(|m| m.trim().trim_matches('"').to_string())
.collect::<Vec<_>>()
}),
if_none_match: headers
.get(hdr_if_none_match)
.map(|x| x.to_str())
.transpose()?
.map(|x| {
x.split(',')
.map(|m| m.trim().trim_matches('"').to_string())
.collect::<Vec<_>>()
}),
if_modified_since: headers
.get(hdr_if_modified_since)
.map(|x| x.to_str())
.transpose()?
.map(httpdate::parse_http_date)
.transpose()
.ok_or_bad_request("Invalid date in if-modified-since")?,
if_unmodified_since: headers
.get(hdr_if_unmodified_since)
.map(|x| x.to_str())
.transpose()?
.map(httpdate::parse_http_date)
.transpose()
.ok_or_bad_request("Invalid date in if-unmodified-since")?,
})
}
fn check(&self, v: &ObjectVersion, etag: &str) -> Result<Option<StatusCode>, Error> {
let v_date = UNIX_EPOCH + Duration::from_millis(v.timestamp);
// Implemented from https://datatracker.ietf.org/doc/html/rfc7232#section-6
if let Some(im) = &self.if_match {
// Step 1: if-match is present
if !im.iter().any(|x| x == etag || x == "*") {
return Ok(Some(StatusCode::PRECONDITION_FAILED));
}
} else if let Some(ius) = &self.if_unmodified_since {
// Step 2: if-unmodified-since is present, and if-match is absent
if v_date > *ius {
return Ok(Some(StatusCode::PRECONDITION_FAILED));
}
}
if let Some(inm) = &self.if_none_match {
// Step 3: if-none-match is present
if inm.iter().any(|x| x == etag || x == "*") {
return Ok(Some(StatusCode::NOT_MODIFIED));
}
} else if let Some(ims) = &self.if_modified_since {
// Step 4: if-modified-since is present, and if-none-match is absent
if v_date <= *ims {
return Ok(Some(StatusCode::NOT_MODIFIED));
}
}
Ok(None)
}
pub(crate) fn check_copy_source(&self, v: &ObjectVersion, etag: &str) -> Result<(), Error> {
match self.check(v, etag)? {
Some(_) => Err(Error::PreconditionFailed),
None => Ok(()),
}
}
}
+1 -2
View File
@@ -14,9 +14,8 @@ mod list;
mod multipart; mod multipart;
mod post_object; mod post_object;
mod put; mod put;
mod website; pub mod website;
mod checksum;
mod encryption; mod encryption;
mod router; mod router;
pub mod xml; pub mod xml;
+1 -9
View File
@@ -1,12 +1,10 @@
use quick_xml::de::from_reader; use quick_xml::de::from_reader;
use http_body_util::BodyExt;
use hyper::{Request, Response, StatusCode}; use hyper::{Request, Response, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::verify_signed_content;
use crate::api_server::{ReqBody, ResBody}; use crate::api_server::{ReqBody, ResBody};
use crate::error::*; use crate::error::*;
@@ -16,7 +14,6 @@ use garage_model::bucket_table::{
parse_lifecycle_date, Bucket, LifecycleExpiration as GarageLifecycleExpiration, parse_lifecycle_date, Bucket, LifecycleExpiration as GarageLifecycleExpiration,
LifecycleFilter as GarageLifecycleFilter, LifecycleRule as GarageLifecycleRule, LifecycleFilter as GarageLifecycleFilter, LifecycleRule as GarageLifecycleRule,
}; };
use garage_util::data::*;
pub async fn handle_get_lifecycle(ctx: ReqCtx) -> Result<Response<ResBody>, Error> { pub async fn handle_get_lifecycle(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
let ReqCtx { bucket_params, .. } = ctx; let ReqCtx { bucket_params, .. } = ctx;
@@ -56,7 +53,6 @@ pub async fn handle_delete_lifecycle(ctx: ReqCtx) -> Result<Response<ResBody>, E
pub async fn handle_put_lifecycle( pub async fn handle_put_lifecycle(
ctx: ReqCtx, ctx: ReqCtx,
req: Request<ReqBody>, req: Request<ReqBody>,
content_sha256: Option<Hash>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let ReqCtx { let ReqCtx {
garage, garage,
@@ -65,11 +61,7 @@ pub async fn handle_put_lifecycle(
.. ..
} = ctx; } = ctx;
let body = BodyExt::collect(req.into_body()).await?.to_bytes(); let body = req.into_body().collect().await?;
if let Some(content_sha256) = content_sha256 {
verify_signed_content(content_sha256, &body[..])?;
}
let conf: LifecycleConfiguration = from_reader(&body as &[u8])?; let conf: LifecycleConfiguration = from_reader(&body as &[u8])?;
let config = conf let config = conf
+131 -22
View File
@@ -1,13 +1,20 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto; use std::convert::{TryFrom, TryInto};
use std::hash::Hasher;
use std::sync::Arc; use std::sync::Arc;
use base64::prelude::*; use base64::prelude::*;
use crc32c::Crc32cHasher as Crc32c;
use crc32fast::Hasher as Crc32;
use futures::prelude::*; use futures::prelude::*;
use hyper::{Request, Response}; use hyper::{Request, Response};
use md5::{Digest, Md5};
use sha1::Sha1;
use sha2::Sha256;
use garage_table::*; use garage_table::*;
use garage_util::data::*; use garage_util::data::*;
use garage_util::error::OkOrMessage;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::s3::block_ref_table::*; use garage_model::s3::block_ref_table::*;
@@ -16,10 +23,9 @@ use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*; use garage_model::s3::version_table::*;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::verify_signed_content; use garage_api_common::signature::checksum::*;
use crate::api_server::{ReqBody, ResBody}; use crate::api_server::{ReqBody, ResBody};
use crate::checksum::*;
use crate::encryption::EncryptionParams; use crate::encryption::EncryptionParams;
use crate::error::*; use crate::error::*;
use crate::put::*; use crate::put::*;
@@ -43,7 +49,7 @@ pub async fn handle_create_multipart_upload(
let upload_id = gen_uuid(); let upload_id = gen_uuid();
let timestamp = next_timestamp(existing_object.as_ref()); let timestamp = next_timestamp(existing_object.as_ref());
let headers = get_headers(req.headers())?; let headers = extract_metadata_headers(req.headers())?;
let meta = ObjectVersionMetaInner { let meta = ObjectVersionMetaInner {
headers, headers,
checksum: None, checksum: None,
@@ -94,7 +100,6 @@ pub async fn handle_put_part(
key: &str, key: &str,
part_number: u64, part_number: u64,
upload_id: &str, upload_id: &str,
content_sha256: Option<Hash>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let ReqCtx { garage, .. } = &ctx; let ReqCtx { garage, .. } = &ctx;
@@ -105,17 +110,30 @@ pub async fn handle_put_part(
Some(x) => Some(x.to_str()?.to_string()), Some(x) => Some(x.to_str()?.to_string()),
None => None, None => None,
}, },
sha256: content_sha256, sha256: None,
extra: request_checksum_value(req.headers())?, extra: request_checksum_value(req.headers())?,
}; };
// Read first chuck, and at the same time try to get object to see if it exists
let key = key.to_string(); let key = key.to_string();
let (req_head, req_body) = req.into_parts(); let (req_head, mut req_body) = req.into_parts();
let stream = body_stream(req_body);
// Before we stream the body, configure the needed checksums.
req_body.add_expected_checksums(expected_checksums.clone());
// TODO: avoid parsing encryption headers twice...
if !EncryptionParams::new_from_headers(&garage, &req_head.headers)?.is_encrypted() {
// For non-encrypted objects, we need to compute the md5sum in all cases
// (even if content-md5 is not set), because it is used as an etag of the
// part, which is in turn used in the etag computation of the whole object
req_body.add_md5();
}
let (stream, stream_checksums) = req_body.streaming_with_checksums();
let stream = stream.map_err(Error::from);
let mut chunker = StreamChunker::new(stream, garage.config.block_size); let mut chunker = StreamChunker::new(stream, garage.config.block_size);
// Read first chuck, and at the same time try to get object to see if it exists
let ((_, object_version, mut mpu), first_block) = let ((_, object_version, mut mpu), first_block) =
futures::try_join!(get_upload(&ctx, &key, &upload_id), chunker.next(),)?; futures::try_join!(get_upload(&ctx, &key, &upload_id), chunker.next(),)?;
@@ -172,21 +190,21 @@ pub async fn handle_put_part(
garage.version_table.insert(&version).await?; garage.version_table.insert(&version).await?;
// Copy data to version // Copy data to version
let checksummer = let (total_size, _, _) = read_and_put_blocks(
Checksummer::init(&expected_checksums, !encryption.is_encrypted()).add(checksum_algorithm);
let (total_size, checksums, _) = read_and_put_blocks(
&ctx, &ctx,
&version, &version,
encryption, encryption,
part_number, part_number,
first_block, first_block,
&mut chunker, chunker,
checksummer, Checksummer::new(),
) )
.await?; .await?;
// Verify that checksums map // Verify that checksums match
checksums.verify(&expected_checksums)?; let checksums = stream_checksums
.await
.ok_or_internal_error("checksum calculation")??;
// Store part etag in version // Store part etag in version
let etag = encryption.etag_from_md5(&checksums.md5); let etag = encryption.etag_from_md5(&checksums.md5);
@@ -248,7 +266,6 @@ pub async fn handle_complete_multipart_upload(
req: Request<ReqBody>, req: Request<ReqBody>,
key: &str, key: &str,
upload_id: &str, upload_id: &str,
content_sha256: Option<Hash>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let ReqCtx { let ReqCtx {
garage, garage,
@@ -260,11 +277,7 @@ pub async fn handle_complete_multipart_upload(
let expected_checksum = request_checksum_value(&req_head.headers)?; let expected_checksum = request_checksum_value(&req_head.headers)?;
let body = http_body_util::BodyExt::collect(req_body).await?.to_bytes(); let body = req_body.collect().await?;
if let Some(content_sha256) = content_sha256 {
verify_signed_content(content_sha256, &body[..])?;
}
let body_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?; let body_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?;
let body_list_of_parts = parse_complete_multipart_upload_body(&body_xml) let body_list_of_parts = parse_complete_multipart_upload_body(&body_xml)
@@ -602,3 +615,99 @@ fn parse_complete_multipart_upload_body(
Some(parts) Some(parts)
} }
// ====== checksummer ====
#[derive(Default)]
pub(crate) struct MultipartChecksummer {
pub md5: Md5,
pub extra: Option<MultipartExtraChecksummer>,
}
pub(crate) enum MultipartExtraChecksummer {
Crc32(Crc32),
Crc32c(Crc32c),
Sha1(Sha1),
Sha256(Sha256),
}
impl MultipartChecksummer {
pub(crate) fn init(algo: Option<ChecksumAlgorithm>) -> Self {
Self {
md5: Md5::new(),
extra: match algo {
None => None,
Some(ChecksumAlgorithm::Crc32) => {
Some(MultipartExtraChecksummer::Crc32(Crc32::new()))
}
Some(ChecksumAlgorithm::Crc32c) => {
Some(MultipartExtraChecksummer::Crc32c(Crc32c::default()))
}
Some(ChecksumAlgorithm::Sha1) => Some(MultipartExtraChecksummer::Sha1(Sha1::new())),
Some(ChecksumAlgorithm::Sha256) => {
Some(MultipartExtraChecksummer::Sha256(Sha256::new()))
}
},
}
}
pub(crate) fn update(
&mut self,
etag: &str,
checksum: Option<ChecksumValue>,
) -> Result<(), Error> {
self.md5
.update(&hex::decode(&etag).ok_or_message("invalid etag hex")?);
match (&mut self.extra, checksum) {
(None, _) => (),
(
Some(MultipartExtraChecksummer::Crc32(ref mut crc32)),
Some(ChecksumValue::Crc32(x)),
) => {
crc32.update(&x);
}
(
Some(MultipartExtraChecksummer::Crc32c(ref mut crc32c)),
Some(ChecksumValue::Crc32c(x)),
) => {
crc32c.write(&x);
}
(Some(MultipartExtraChecksummer::Sha1(ref mut sha1)), Some(ChecksumValue::Sha1(x))) => {
sha1.update(&x);
}
(
Some(MultipartExtraChecksummer::Sha256(ref mut sha256)),
Some(ChecksumValue::Sha256(x)),
) => {
sha256.update(&x);
}
(Some(_), b) => {
return Err(Error::internal_error(format!(
"part checksum was not computed correctly, got: {:?}",
b
)))
}
}
Ok(())
}
pub(crate) fn finalize(self) -> (Md5Checksum, Option<ChecksumValue>) {
let md5 = self.md5.finalize()[..].try_into().unwrap();
let extra = match self.extra {
None => None,
Some(MultipartExtraChecksummer::Crc32(crc32)) => {
Some(ChecksumValue::Crc32(u32::to_be_bytes(crc32.finalize())))
}
Some(MultipartExtraChecksummer::Crc32c(crc32c)) => Some(ChecksumValue::Crc32c(
u32::to_be_bytes(u32::try_from(crc32c.finish()).unwrap()),
)),
Some(MultipartExtraChecksummer::Sha1(sha1)) => {
Some(ChecksumValue::Sha1(sha1.finalize()[..].try_into().unwrap()))
}
Some(MultipartExtraChecksummer::Sha256(sha256)) => Some(ChecksumValue::Sha256(
sha256.finalize()[..].try_into().unwrap(),
)),
};
(md5, extra)
}
}
+7 -4
View File
@@ -18,13 +18,13 @@ use garage_model::s3::object_table::*;
use garage_api_common::cors::*; use garage_api_common::cors::*;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::checksum::*;
use garage_api_common::signature::payload::{verify_v4, Authorization}; use garage_api_common::signature::payload::{verify_v4, Authorization};
use crate::api_server::ResBody; use crate::api_server::ResBody;
use crate::checksum::*;
use crate::encryption::EncryptionParams; use crate::encryption::EncryptionParams;
use crate::error::*; 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; use crate::xml as s3_xml;
pub async fn handle_post_object( pub async fn handle_post_object(
@@ -216,8 +216,9 @@ pub async fn handle_post_object(
// if we ever start supporting ACLs, we likely want to map "acl" to x-amz-acl" somewhere // 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. // 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 { let expected_checksums = ExpectedChecksums {
md5: params md5: params
.get("content-md5") .get("content-md5")
@@ -225,7 +226,9 @@ pub async fn handle_post_object(
.transpose()? .transpose()?
.map(str::to_string), .map(str::to_string),
sha256: None, sha256: None,
extra: request_checksum_algorithm_value(&params)?, extra: checksum_algorithm
.map(|algo| extract_checksum_value(&params, algo))
.transpose()?,
}; };
let meta = ObjectVersionMetaInner { let meta = ObjectVersionMetaInner {
+76 -13
View File
@@ -31,11 +31,13 @@ use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*; use garage_model::s3::version_table::*;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::body::StreamingChecksumReceiver;
use garage_api_common::signature::checksum::*;
use crate::api_server::{ReqBody, ResBody}; use crate::api_server::{ReqBody, ResBody};
use crate::checksum::*;
use crate::encryption::EncryptionParams; use crate::encryption::EncryptionParams;
use crate::error::*; use crate::error::*;
use crate::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
const PUT_BLOCKS_MAX_PARALLEL: usize = 3; const PUT_BLOCKS_MAX_PARALLEL: usize = 3;
@@ -48,6 +50,10 @@ pub(crate) struct SaveStreamResult {
pub(crate) enum ChecksumMode<'a> { pub(crate) enum ChecksumMode<'a> {
Verify(&'a ExpectedChecksums), Verify(&'a ExpectedChecksums),
VerifyFrom {
checksummer: StreamingChecksumReceiver,
trailer_algo: Option<ChecksumAlgorithm>,
},
Calculate(Option<ChecksumAlgorithm>), Calculate(Option<ChecksumAlgorithm>),
} }
@@ -55,10 +61,9 @@ pub async fn handle_put(
ctx: ReqCtx, ctx: ReqCtx,
req: Request<ReqBody>, req: Request<ReqBody>,
key: &String, key: &String,
content_sha256: Option<Hash>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
// Retrieve interesting headers from request // Retrieve interesting headers from request
let headers = get_headers(req.headers())?; let headers = extract_metadata_headers(req.headers())?;
debug!("Object headers: {:?}", headers); debug!("Object headers: {:?}", headers);
let expected_checksums = ExpectedChecksums { let expected_checksums = ExpectedChecksums {
@@ -66,9 +71,10 @@ pub async fn handle_put(
Some(x) => Some(x.to_str()?.to_string()), Some(x) => Some(x.to_str()?.to_string()),
None => None, None => None,
}, },
sha256: content_sha256, sha256: None,
extra: request_checksum_value(req.headers())?, extra: request_checksum_value(req.headers())?,
}; };
let trailer_checksum_algorithm = request_trailer_checksum_algorithm(req.headers())?;
let meta = ObjectVersionMetaInner { let meta = ObjectVersionMetaInner {
headers, headers,
@@ -78,7 +84,19 @@ pub async fn handle_put(
// Determine whether object should be encrypted, and if so the key // Determine whether object should be encrypted, and if so the key
let encryption = EncryptionParams::new_from_headers(&ctx.garage, req.headers())?; let encryption = EncryptionParams::new_from_headers(&ctx.garage, req.headers())?;
let stream = body_stream(req.into_body()); // The request body is a special ReqBody object (see garage_api_common::signature::body)
// which supports calculating checksums while streaming the data.
// Before we start streaming, we configure it to calculate all the checksums we need.
let mut req_body = req.into_body();
req_body.add_expected_checksums(expected_checksums.clone());
if !encryption.is_encrypted() {
// For non-encrypted objects, we need to compute the md5sum in all cases
// (even if content-md5 is not set), because it is used as the object etag
req_body.add_md5();
}
let (stream, checksummer) = req_body.streaming_with_checksums();
let stream = stream.map_err(Error::from);
let res = save_stream( let res = save_stream(
&ctx, &ctx,
@@ -86,7 +104,10 @@ pub async fn handle_put(
encryption, encryption,
stream, stream,
key, key,
ChecksumMode::Verify(&expected_checksums), ChecksumMode::VerifyFrom {
checksummer,
trailer_algo: trailer_checksum_algorithm,
},
) )
.await?; .await?;
@@ -122,10 +143,15 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
let version_uuid = gen_uuid(); let version_uuid = gen_uuid();
let version_timestamp = next_timestamp(existing_object.as_ref()); let version_timestamp = next_timestamp(existing_object.as_ref());
let mut checksummer = match checksum_mode { let mut checksummer = match &checksum_mode {
ChecksumMode::Verify(expected) => Checksummer::init(expected, !encryption.is_encrypted()), ChecksumMode::Verify(expected) => Checksummer::init(expected, !encryption.is_encrypted()),
ChecksumMode::Calculate(algo) => { ChecksumMode::Calculate(algo) => {
Checksummer::init(&Default::default(), !encryption.is_encrypted()).add(algo) Checksummer::init(&Default::default(), !encryption.is_encrypted()).add(*algo)
}
ChecksumMode::VerifyFrom { .. } => {
// Checksums are calculated by the garage_api_common::signature module
// so here we can just have an empty checksummer that does nothing
Checksummer::new()
} }
}; };
@@ -133,7 +159,7 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
// as "inline data". We can then return immediately. // as "inline data". We can then return immediately.
if first_block.len() < INLINE_THRESHOLD { if first_block.len() < INLINE_THRESHOLD {
checksummer.update(&first_block); checksummer.update(&first_block);
let checksums = checksummer.finalize(); let mut checksums = checksummer.finalize();
match checksum_mode { match checksum_mode {
ChecksumMode::Verify(expected) => { ChecksumMode::Verify(expected) => {
@@ -142,6 +168,18 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
ChecksumMode::Calculate(algo) => { ChecksumMode::Calculate(algo) => {
meta.checksum = checksums.extract(algo); meta.checksum = checksums.extract(algo);
} }
ChecksumMode::VerifyFrom {
checksummer,
trailer_algo,
} => {
drop(chunker);
checksums = checksummer
.await
.ok_or_internal_error("checksum calculation")??;
if let Some(algo) = trailer_algo {
meta.checksum = checksums.extract(Some(algo));
}
}
}; };
let size = first_block.len() as u64; let size = first_block.len() as u64;
@@ -213,13 +251,13 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
garage.version_table.insert(&version).await?; garage.version_table.insert(&version).await?;
// Transfer data // Transfer data
let (total_size, checksums, first_block_hash) = read_and_put_blocks( let (total_size, mut checksums, first_block_hash) = read_and_put_blocks(
ctx, ctx,
&version, &version,
encryption, encryption,
1, 1,
first_block, first_block,
&mut chunker, chunker,
checksummer, checksummer,
) )
.await?; .await?;
@@ -232,6 +270,17 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
ChecksumMode::Calculate(algo) => { ChecksumMode::Calculate(algo) => {
meta.checksum = checksums.extract(algo); meta.checksum = checksums.extract(algo);
} }
ChecksumMode::VerifyFrom {
checksummer,
trailer_algo,
} => {
checksums = checksummer
.await
.ok_or_internal_error("checksum calculation")??;
if let Some(algo) = trailer_algo {
meta.checksum = checksums.extract(Some(algo));
}
}
}; };
// Verify quotas are respsected // Verify quotas are respsected
@@ -332,7 +381,7 @@ pub(crate) async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> +
encryption: EncryptionParams, encryption: EncryptionParams,
part_number: u64, part_number: u64,
first_block: Bytes, first_block: Bytes,
chunker: &mut StreamChunker<S>, mut chunker: StreamChunker<S>,
checksummer: Checksummer, checksummer: Checksummer,
) -> Result<(u64, Checksums, Hash), Error> { ) -> Result<(u64, Checksums, Hash), Error> {
let tracer = opentelemetry::global::tracer("garage"); let tracer = opentelemetry::global::tracer("garage");
@@ -601,7 +650,9 @@ impl Drop for InterruptedCleanup {
// ============ helpers ============ // ============ 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(); let mut ret = Vec::new();
// Preserve standard headers // Preserve standard headers
@@ -627,6 +678,18 @@ pub(crate) fn get_headers(headers: &HeaderMap<HeaderValue>) -> Result<HeaderList
std::str::from_utf8(value.as_bytes())?.to_string(), 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) Ok(ret)
+5 -10
View File
@@ -1,19 +1,19 @@
use quick_xml::de::from_reader; use quick_xml::de::from_reader;
use http_body_util::BodyExt; use hyper::{header::HeaderName, Request, Response, StatusCode};
use hyper::{Request, Response, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use garage_model::bucket_table::*; use garage_model::bucket_table::*;
use garage_util::data::*;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use garage_api_common::signature::verify_signed_content;
use crate::api_server::{ReqBody, ResBody}; use crate::api_server::{ReqBody, ResBody};
use crate::error::*; use crate::error::*;
use crate::xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; 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> { pub async fn handle_get_website(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
let ReqCtx { bucket_params, .. } = ctx; let ReqCtx { bucket_params, .. } = ctx;
if let Some(website) = bucket_params.website_config.get() { if let Some(website) = bucket_params.website_config.get() {
@@ -61,7 +61,6 @@ pub async fn handle_delete_website(ctx: ReqCtx) -> Result<Response<ResBody>, Err
pub async fn handle_put_website( pub async fn handle_put_website(
ctx: ReqCtx, ctx: ReqCtx,
req: Request<ReqBody>, req: Request<ReqBody>,
content_sha256: Option<Hash>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let ReqCtx { let ReqCtx {
garage, garage,
@@ -70,11 +69,7 @@ pub async fn handle_put_website(
.. ..
} = ctx; } = ctx;
let body = BodyExt::collect(req.into_body()).await?.to_bytes(); let body = req.into_body().collect().await?;
if let Some(content_sha256) = content_sha256 {
verify_signed_content(content_sha256, &body[..])?;
}
let conf: WebsiteConfiguration = from_reader(&body as &[u8])?; let conf: WebsiteConfiguration = from_reader(&body as &[u8])?;
conf.validate()?; conf.validate()?;
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_block" name = "garage_block"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_db" name = "garage_db"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+3 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage" name = "garage"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -71,10 +71,12 @@ hyper-util.workspace = true
mktemp.workspace = true mktemp.workspace = true
sha2.workspace = true sha2.workspace = true
static_init.workspace = true static_init.workspace = true
assert-json-diff.workspace = true assert-json-diff.workspace = true
serde_json.workspace = true serde_json.workspace = true
base64.workspace = true base64.workspace = true
crc32fast.workspace = true
k2v-client.workspace = true k2v-client.workspace = true
+1 -1
View File
@@ -12,7 +12,7 @@ pub fn build_client(key: &Key) -> Client {
.endpoint_url(format!("http://127.0.0.1:{}", DEFAULT_PORT)) .endpoint_url(format!("http://127.0.0.1:{}", DEFAULT_PORT))
.region(super::REGION) .region(super::REGION)
.credentials_provider(credentials) .credentials_provider(credentials)
.behavior_version(BehaviorVersion::v2023_11_09()) .behavior_version(BehaviorVersion::v2024_03_28())
.build(); .build();
Client::from_conf(config) Client::from_conf(config)
+101 -17
View File
@@ -192,16 +192,13 @@ impl<'a> RequestBuilder<'a> {
.collect::<HeaderMap>(); .collect::<HeaderMap>();
let date = now.format(signature::LONG_DATETIME).to_string(); let date = now.format(signature::LONG_DATETIME).to_string();
all_headers.insert( all_headers.insert(signature::X_AMZ_DATE, HeaderValue::from_str(&date).unwrap());
signature::payload::X_AMZ_DATE,
HeaderValue::from_str(&date).unwrap(),
);
all_headers.insert(HOST, HeaderValue::from_str(&host).unwrap()); all_headers.insert(HOST, HeaderValue::from_str(&host).unwrap());
let body_sha = match self.body_signature { let body_sha = match &self.body_signature {
BodySignature::Unsigned => "UNSIGNED-PAYLOAD".to_owned(), BodySignature::Unsigned => "UNSIGNED-PAYLOAD".to_owned(),
BodySignature::Classic => hex::encode(garage_util::data::sha256sum(&self.body)), BodySignature::Classic => hex::encode(garage_util::data::sha256sum(&self.body)),
BodySignature::Streaming(size) => { BodySignature::Streaming { chunk_size } => {
all_headers.insert( all_headers.insert(
CONTENT_ENCODING, CONTENT_ENCODING,
HeaderValue::from_str("aws-chunked").unwrap(), HeaderValue::from_str("aws-chunked").unwrap(),
@@ -216,18 +213,59 @@ impl<'a> RequestBuilder<'a> {
// code. // code.
all_headers.insert( all_headers.insert(
CONTENT_LENGTH, CONTENT_LENGTH,
to_streaming_body(&self.body, size, String::new(), signer.clone(), now, "") to_streaming_body(
.len() &self.body,
.to_string() *chunk_size,
.try_into() String::new(),
.unwrap(), signer.clone(),
now,
"",
)
.len()
.to_string()
.try_into()
.unwrap(),
); );
"STREAMING-AWS4-HMAC-SHA256-PAYLOAD".to_owned() "STREAMING-AWS4-HMAC-SHA256-PAYLOAD".to_owned()
} }
BodySignature::StreamingUnsignedTrailer {
chunk_size,
trailer_algorithm,
trailer_value,
} => {
all_headers.insert(
CONTENT_ENCODING,
HeaderValue::from_str("aws-chunked").unwrap(),
);
all_headers.insert(
HeaderName::from_static("x-amz-decoded-content-length"),
HeaderValue::from_str(&self.body.len().to_string()).unwrap(),
);
all_headers.insert(
HeaderName::from_static("x-amz-trailer"),
HeaderValue::from_str(&trailer_algorithm).unwrap(),
);
all_headers.insert(
CONTENT_LENGTH,
to_streaming_unsigned_trailer_body(
&self.body,
*chunk_size,
&trailer_algorithm,
&trailer_value,
)
.len()
.to_string()
.try_into()
.unwrap(),
);
"STREAMING-UNSIGNED-PAYLOAD-TRAILER".to_owned()
}
}; };
all_headers.insert( all_headers.insert(
signature::payload::X_AMZ_CONTENT_SH256, signature::X_AMZ_CONTENT_SHA256,
HeaderValue::from_str(&body_sha).unwrap(), HeaderValue::from_str(&body_sha).unwrap(),
); );
@@ -276,10 +314,26 @@ impl<'a> RequestBuilder<'a> {
let mut request = Request::builder(); let mut request = Request::builder();
*request.headers_mut().unwrap() = all_headers; *request.headers_mut().unwrap() = all_headers;
let body = if let BodySignature::Streaming(size) = self.body_signature { let body = match &self.body_signature {
to_streaming_body(&self.body, size, signature, streaming_signer, now, &scope) BodySignature::Streaming { chunk_size } => to_streaming_body(
} else { &self.body,
self.body.clone() *chunk_size,
signature,
streaming_signer,
now,
&scope,
),
BodySignature::StreamingUnsignedTrailer {
chunk_size,
trailer_algorithm,
trailer_value,
} => to_streaming_unsigned_trailer_body(
&self.body,
*chunk_size,
&trailer_algorithm,
&trailer_value,
),
_ => self.body.clone(),
}; };
let request = request let request = request
.uri(uri) .uri(uri)
@@ -308,7 +362,14 @@ impl<'a> RequestBuilder<'a> {
pub enum BodySignature { pub enum BodySignature {
Unsigned, Unsigned,
Classic, Classic,
Streaming(usize), Streaming {
chunk_size: usize,
},
StreamingUnsignedTrailer {
chunk_size: usize,
trailer_algorithm: String,
trailer_value: String,
},
} }
fn query_param_to_string(params: &HashMap<String, Option<String>>) -> String { fn query_param_to_string(params: &HashMap<String, Option<String>>) -> String {
@@ -363,3 +424,26 @@ fn to_streaming_body(
res res
} }
fn to_streaming_unsigned_trailer_body(
body: &[u8],
chunk_size: usize,
trailer_algorithm: &str,
trailer_value: &str,
) -> Vec<u8> {
let mut res = Vec::with_capacity(body.len());
for chunk in body.chunks(chunk_size) {
let header = format!("{:x}\r\n", chunk.len());
res.extend_from_slice(header.as_bytes());
res.extend_from_slice(chunk);
res.extend_from_slice(b"\r\n");
}
res.extend_from_slice(b"0\r\n");
res.extend_from_slice(trailer_algorithm.as_bytes());
res.extend_from_slice(b":");
res.extend_from_slice(trailer_value.as_bytes());
res.extend_from_slice(b"\n\r\n\r\n");
res
}
+4 -1
View File
@@ -99,7 +99,10 @@ api_bind_addr = "127.0.0.1:{admin_port}"
.arg("server") .arg("server")
.stdout(stdout) .stdout(stdout)
.stderr(stderr) .stderr(stderr)
.env("RUST_LOG", "garage=debug,garage_api=trace") .env(
"RUST_LOG",
"garage=debug,garage_api_common=trace,garage_api_s3=trace",
)
.spawn() .spawn()
.expect("Could not start garage"); .expect("Could not start garage");
+142 -7
View File
@@ -1,5 +1,6 @@
use crate::common; use crate::common;
use aws_sdk_s3::primitives::ByteStream; use aws_sdk_s3::error::SdkError;
use aws_sdk_s3::primitives::{ByteStream, DateTime};
use aws_sdk_s3::types::{Delete, ObjectIdentifier}; use aws_sdk_s3::types::{Delete, ObjectIdentifier};
const STD_KEY: &str = "hello world"; const STD_KEY: &str = "hello world";
@@ -125,6 +126,129 @@ async fn test_putobject() {
} }
} }
#[tokio::test]
async fn test_precondition() {
let ctx = common::context();
let bucket = ctx.create_bucket("precondition");
let etag = "\"46cf18a9b447991b450cad3facf5937e\"";
let etag2 = "\"ae4984b984cd984fe98d4efa954dce98\"";
let data = ByteStream::from_static(BODY);
let r = ctx
.client
.put_object()
.bucket(&bucket)
.key(STD_KEY)
.body(data)
.send()
.await
.unwrap();
assert_eq!(r.e_tag.unwrap().as_str(), etag);
let last_modified;
{
let o = ctx
.client
.get_object()
.bucket(&bucket)
.key(STD_KEY)
.if_match(etag)
.send()
.await
.unwrap();
assert_eq!(o.e_tag.as_ref().unwrap().as_str(), etag);
last_modified = o.last_modified.unwrap();
let err = ctx
.client
.get_object()
.bucket(&bucket)
.key(STD_KEY)
.if_match(etag2)
.send()
.await;
assert!(
matches!(err, Err(SdkError::ServiceError(se)) if se.raw().status().as_u16() == 412)
);
}
{
let o = ctx
.client
.get_object()
.bucket(&bucket)
.key(STD_KEY)
.if_none_match(etag2)
.send()
.await
.unwrap();
assert_eq!(o.e_tag.as_ref().unwrap().as_str(), etag);
let err = ctx
.client
.get_object()
.bucket(&bucket)
.key(STD_KEY)
.if_none_match(etag)
.send()
.await;
assert!(
matches!(err, Err(SdkError::ServiceError(se)) if se.raw().status().as_u16() == 304)
);
}
let older_date = DateTime::from_secs_f64(last_modified.as_secs_f64() - 10.0);
let newer_date = DateTime::from_secs_f64(last_modified.as_secs_f64() + 10.0);
{
let err = ctx
.client
.get_object()
.bucket(&bucket)
.key(STD_KEY)
.if_modified_since(newer_date)
.send()
.await;
assert!(
matches!(err, Err(SdkError::ServiceError(se)) if se.raw().status().as_u16() == 304)
);
let o = ctx
.client
.get_object()
.bucket(&bucket)
.key(STD_KEY)
.if_modified_since(older_date)
.send()
.await
.unwrap();
assert_eq!(o.e_tag.as_ref().unwrap().as_str(), etag);
}
{
let err = ctx
.client
.get_object()
.bucket(&bucket)
.key(STD_KEY)
.if_unmodified_since(older_date)
.send()
.await;
assert!(
matches!(err, Err(SdkError::ServiceError(se)) if se.raw().status().as_u16() == 412)
);
let o = ctx
.client
.get_object()
.bucket(&bucket)
.key(STD_KEY)
.if_unmodified_since(newer_date)
.send()
.await
.unwrap();
assert_eq!(o.e_tag.as_ref().unwrap().as_str(), etag);
}
}
#[tokio::test] #[tokio::test]
async fn test_getobject() { async fn test_getobject() {
let ctx = common::context(); let ctx = common::context();
@@ -189,12 +313,14 @@ async fn test_getobject() {
#[tokio::test] #[tokio::test]
async fn test_metadata() { async fn test_metadata() {
use aws_sdk_s3::primitives::{DateTime, DateTimeFormat};
let ctx = common::context(); let ctx = common::context();
let bucket = ctx.create_bucket("testmetadata"); let bucket = ctx.create_bucket("testmetadata");
let etag = "\"46cf18a9b447991b450cad3facf5937e\""; let etag = "\"46cf18a9b447991b450cad3facf5937e\"";
let exp = aws_sdk_s3::primitives::DateTime::from_secs(10000000000); let exp = DateTime::from_secs(10000000000);
let exp2 = aws_sdk_s3::primitives::DateTime::from_secs(10000500000); let exp2 = DateTime::from_secs(10000500000);
{ {
// Note. The AWS client SDK adds a Content-Type header // Note. The AWS client SDK adds a Content-Type header
@@ -227,7 +353,7 @@ async fn test_metadata() {
assert_eq!(o.content_disposition, None); assert_eq!(o.content_disposition, None);
assert_eq!(o.content_encoding, None); assert_eq!(o.content_encoding, None);
assert_eq!(o.content_language, None); assert_eq!(o.content_language, None);
assert_eq!(o.expires, None); assert_eq!(o.expires_string, None);
assert_eq!(o.metadata.unwrap_or_default().len(), 0); assert_eq!(o.metadata.unwrap_or_default().len(), 0);
let o = ctx let o = ctx
@@ -250,7 +376,10 @@ async fn test_metadata() {
assert_eq!(o.content_disposition.unwrap().as_str(), "cddummy"); assert_eq!(o.content_disposition.unwrap().as_str(), "cddummy");
assert_eq!(o.content_encoding.unwrap().as_str(), "cedummy"); assert_eq!(o.content_encoding.unwrap().as_str(), "cedummy");
assert_eq!(o.content_language.unwrap().as_str(), "cldummy"); assert_eq!(o.content_language.unwrap().as_str(), "cldummy");
assert_eq!(o.expires.unwrap(), exp); assert_eq!(
o.expires_string.unwrap(),
exp.fmt(DateTimeFormat::HttpDate).unwrap()
);
} }
{ {
@@ -288,7 +417,10 @@ async fn test_metadata() {
assert_eq!(o.content_disposition.unwrap().as_str(), "cdtest"); assert_eq!(o.content_disposition.unwrap().as_str(), "cdtest");
assert_eq!(o.content_encoding.unwrap().as_str(), "cetest"); assert_eq!(o.content_encoding.unwrap().as_str(), "cetest");
assert_eq!(o.content_language.unwrap().as_str(), "cltest"); assert_eq!(o.content_language.unwrap().as_str(), "cltest");
assert_eq!(o.expires.unwrap(), exp2); assert_eq!(
o.expires_string.unwrap(),
exp2.fmt(DateTimeFormat::HttpDate).unwrap()
);
let mut meta = o.metadata.unwrap(); let mut meta = o.metadata.unwrap();
assert_eq!(meta.remove("testmeta").unwrap(), "hello people"); assert_eq!(meta.remove("testmeta").unwrap(), "hello people");
assert_eq!(meta.remove("nice-unicode-meta").unwrap(), "宅配便"); assert_eq!(meta.remove("nice-unicode-meta").unwrap(), "宅配便");
@@ -314,7 +446,10 @@ async fn test_metadata() {
assert_eq!(o.content_disposition.unwrap().as_str(), "cddummy"); assert_eq!(o.content_disposition.unwrap().as_str(), "cddummy");
assert_eq!(o.content_encoding.unwrap().as_str(), "cedummy"); assert_eq!(o.content_encoding.unwrap().as_str(), "cedummy");
assert_eq!(o.content_language.unwrap().as_str(), "cldummy"); assert_eq!(o.content_language.unwrap().as_str(), "cldummy");
assert_eq!(o.expires.unwrap(), exp); assert_eq!(
o.expires_string.unwrap(),
exp.fmt(DateTimeFormat::HttpDate).unwrap()
);
} }
} }
+156 -6
View File
@@ -1,5 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use base64::prelude::*;
use crc32fast::Hasher as Crc32;
use crate::common; use crate::common;
use crate::common::ext::CommandExt; use crate::common::ext::CommandExt;
use common::custom_requester::BodySignature; use common::custom_requester::BodySignature;
@@ -21,7 +24,7 @@ async fn test_putobject_streaming() {
let content_type = "text/csv"; let content_type = "text/csv";
let mut headers = HashMap::new(); let mut headers = HashMap::new();
headers.insert("content-type".to_owned(), content_type.to_owned()); headers.insert("content-type".to_owned(), content_type.to_owned());
let _ = ctx let res = ctx
.custom_request .custom_request
.builder(bucket.clone()) .builder(bucket.clone())
.method(Method::PUT) .method(Method::PUT)
@@ -29,10 +32,11 @@ async fn test_putobject_streaming() {
.signed_headers(headers) .signed_headers(headers)
.vhost_style(true) .vhost_style(true)
.body(vec![]) .body(vec![])
.body_signature(BodySignature::Streaming(10)) .body_signature(BodySignature::Streaming { chunk_size: 10 })
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(res.status().is_success(), "got response: {:?}", res);
// assert_eq!(r.e_tag.unwrap().as_str(), etag); // assert_eq!(r.e_tag.unwrap().as_str(), etag);
// We return a version ID here // We return a version ID here
@@ -65,7 +69,14 @@ async fn test_putobject_streaming() {
{ {
let etag = "\"46cf18a9b447991b450cad3facf5937e\""; let etag = "\"46cf18a9b447991b450cad3facf5937e\"";
let _ = ctx let mut crc32 = Crc32::new();
crc32.update(&BODY[..]);
let crc32 = BASE64_STANDARD.encode(&u32::to_be_bytes(crc32.finalize())[..]);
let mut headers = HashMap::new();
headers.insert("x-amz-checksum-crc32".to_owned(), crc32.clone());
let res = ctx
.custom_request .custom_request
.builder(bucket.clone()) .builder(bucket.clone())
.method(Method::PUT) .method(Method::PUT)
@@ -73,11 +84,13 @@ async fn test_putobject_streaming() {
//fail //fail
.path("abc".to_owned()) .path("abc".to_owned())
.vhost_style(true) .vhost_style(true)
.signed_headers(headers)
.body(BODY.to_vec()) .body(BODY.to_vec())
.body_signature(BodySignature::Streaming(16)) .body_signature(BodySignature::Streaming { chunk_size: 16 })
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(res.status().is_success(), "got response: {:?}", res);
// assert_eq!(r.e_tag.unwrap().as_str(), etag); // assert_eq!(r.e_tag.unwrap().as_str(), etag);
// assert!(r.version_id.is_some()); // assert!(r.version_id.is_some());
@@ -88,6 +101,7 @@ async fn test_putobject_streaming() {
.bucket(&bucket) .bucket(&bucket)
//.key(CTRL_KEY) //.key(CTRL_KEY)
.key("abc") .key("abc")
.checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled)
.send() .send()
.await .await
.unwrap(); .unwrap();
@@ -98,6 +112,142 @@ async fn test_putobject_streaming() {
assert_eq!(o.content_length.unwrap(), 62); assert_eq!(o.content_length.unwrap(), 62);
assert_eq!(o.parts_count, None); assert_eq!(o.parts_count, None);
assert_eq!(o.tag_count, None); assert_eq!(o.tag_count, None);
assert_eq!(o.checksum_crc32.unwrap(), crc32);
}
}
#[tokio::test]
async fn test_putobject_streaming_unsigned_trailer() {
let ctx = common::context();
let bucket = ctx.create_bucket("putobject-streaming-unsigned-trailer");
{
// Send an empty object (can serve as a directory marker)
// with a content type
let etag = "\"d41d8cd98f00b204e9800998ecf8427e\"";
let content_type = "text/csv";
let mut headers = HashMap::new();
headers.insert("content-type".to_owned(), content_type.to_owned());
let empty_crc32 = BASE64_STANDARD.encode(&u32::to_be_bytes(Crc32::new().finalize())[..]);
let res = ctx
.custom_request
.builder(bucket.clone())
.method(Method::PUT)
.path(STD_KEY.to_owned())
.signed_headers(headers)
.vhost_style(true)
.body(vec![])
.body_signature(BodySignature::StreamingUnsignedTrailer {
chunk_size: 10,
trailer_algorithm: "x-amz-checksum-crc32".into(),
trailer_value: empty_crc32,
})
.send()
.await
.unwrap();
assert!(res.status().is_success(), "got response: {:?}", res);
// assert_eq!(r.e_tag.unwrap().as_str(), etag);
// We return a version ID here
// We should check if Amazon is returning one when versioning is not enabled
// assert!(r.version_id.is_some());
//let _version = r.version_id.unwrap();
let o = ctx
.client
.get_object()
.bucket(&bucket)
.key(STD_KEY)
.send()
.await
.unwrap();
assert_bytes_eq!(o.body, b"");
assert_eq!(o.e_tag.unwrap(), etag);
// We do not return version ID
// We should check if Amazon is returning one when versioning is not enabled
// assert_eq!(o.version_id.unwrap(), _version);
assert_eq!(o.content_type.unwrap(), content_type);
assert!(o.last_modified.is_some());
assert_eq!(o.content_length.unwrap(), 0);
assert_eq!(o.parts_count, None);
assert_eq!(o.tag_count, None);
}
{
let etag = "\"46cf18a9b447991b450cad3facf5937e\"";
let mut crc32 = Crc32::new();
crc32.update(&BODY[..]);
let crc32 = BASE64_STANDARD.encode(&u32::to_be_bytes(crc32.finalize())[..]);
// try sending with wrong crc32, check that it fails
let err_res = ctx
.custom_request
.builder(bucket.clone())
.method(Method::PUT)
//.path(CTRL_KEY.to_owned()) at the moment custom_request does not encode url so this
//fail
.path("abc".to_owned())
.vhost_style(true)
.body(BODY.to_vec())
.body_signature(BodySignature::StreamingUnsignedTrailer {
chunk_size: 16,
trailer_algorithm: "x-amz-checksum-crc32".into(),
trailer_value: "2Yp9Yw==".into(),
})
.send()
.await
.unwrap();
assert!(
err_res.status().is_client_error(),
"got response: {:?}",
err_res
);
let res = ctx
.custom_request
.builder(bucket.clone())
.method(Method::PUT)
//.path(CTRL_KEY.to_owned()) at the moment custom_request does not encode url so this
//fail
.path("abc".to_owned())
.vhost_style(true)
.body(BODY.to_vec())
.body_signature(BodySignature::StreamingUnsignedTrailer {
chunk_size: 16,
trailer_algorithm: "x-amz-checksum-crc32".into(),
trailer_value: crc32.clone(),
})
.send()
.await
.unwrap();
assert!(res.status().is_success(), "got response: {:?}", res);
// assert_eq!(r.e_tag.unwrap().as_str(), etag);
// assert!(r.version_id.is_some());
let o = ctx
.client
.get_object()
.bucket(&bucket)
//.key(CTRL_KEY)
.key("abc")
.checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled)
.send()
.await
.unwrap();
assert_bytes_eq!(o.body, BODY);
assert_eq!(o.e_tag.unwrap(), etag);
assert!(o.last_modified.is_some());
assert_eq!(o.content_length.unwrap(), 62);
assert_eq!(o.parts_count, None);
assert_eq!(o.tag_count, None);
assert_eq!(o.checksum_crc32.unwrap(), crc32);
} }
} }
@@ -119,7 +269,7 @@ async fn test_create_bucket_streaming() {
.custom_request .custom_request
.builder(bucket.to_owned()) .builder(bucket.to_owned())
.method(Method::PUT) .method(Method::PUT)
.body_signature(BodySignature::Streaming(10)) .body_signature(BodySignature::Streaming { chunk_size: 10 })
.send() .send()
.await .await
.unwrap(); .unwrap();
@@ -174,7 +324,7 @@ async fn test_put_website_streaming() {
.method(Method::PUT) .method(Method::PUT)
.query_params(query) .query_params(query)
.body(website_config.as_bytes().to_vec()) .body(website_config.as_bytes().to_vec())
.body_signature(BodySignature::Streaming(10)) .body_signature(BodySignature::Streaming { chunk_size: 10 })
.send() .send()
.await .await
.unwrap(); .unwrap();
+28
View File
@@ -11,6 +11,7 @@ use http::{Request, StatusCode};
use http_body_util::BodyExt; use http_body_util::BodyExt;
use http_body_util::Full as FullBody; use http_body_util::Full as FullBody;
use hyper::body::Bytes; use hyper::body::Bytes;
use hyper::header::LOCATION;
use hyper_util::client::legacy::Client; use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor; use hyper_util::rt::TokioExecutor;
use serde_json::json; 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 // Test CORS with an allowed preflight request
{ {
let req = Request::builder() let req = Request::builder()
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_model" name = "garage_model"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_net" name = "garage_net"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_rpc" name = "garage_rpc"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+10 -4
View File
@@ -650,8 +650,11 @@ impl LayoutVersion {
let mut cost = CostFunction::new(); let mut cost = CostFunction::new();
for (p, assoc_p) in prev_assign.iter().enumerate() { for (p, assoc_p) in prev_assign.iter().enumerate() {
for n in assoc_p.iter() { for n in assoc_p.iter() {
let node_zone = zone_to_id[self.expect_get_node_zone(&self.node_id_vec[*n])]; if let Some(&node_zone) =
cost.insert((Vertex::PZ(p, node_zone), Vertex::N(*n)), -1); zone_to_id.get(self.expect_get_node_zone(&self.node_id_vec[*n]))
{
cost.insert((Vertex::PZ(p, node_zone), Vertex::N(*n)), -1);
}
} }
} }
@@ -751,8 +754,11 @@ impl LayoutVersion {
if let Some(prev_assign) = prev_assign_opt { if let Some(prev_assign) = prev_assign_opt {
let mut old_zones_of_p = Vec::<usize>::new(); let mut old_zones_of_p = Vec::<usize>::new();
for n in prev_assign[p].iter() { for n in prev_assign[p].iter() {
old_zones_of_p if let Some(&zone_id) =
.push(zone_to_id[self.expect_get_node_zone(&self.node_id_vec[*n])]); zone_to_id.get(self.expect_get_node_zone(&self.node_id_vec[*n]))
{
old_zones_of_p.push(zone_id);
}
} }
if !old_zones_of_p.contains(&z) { if !old_zones_of_p.contains(&z) {
new_partitions_zone[z] += 1; new_partitions_zone[z] += 1;
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_table" name = "garage_table"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_util" name = "garage_util"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_web" name = "garage_web"
version = "1.0.1" version = "1.1.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"] authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+16 -5
View File
@@ -1,13 +1,13 @@
use std::fs::{self, Permissions}; use std::fs::{self, Permissions};
use std::os::unix::prelude::PermissionsExt; use std::os::unix::prelude::PermissionsExt;
use std::{convert::Infallible, sync::Arc}; use std::sync::Arc;
use tokio::net::{TcpListener, UnixListener}; use tokio::net::{TcpListener, UnixListener};
use tokio::sync::watch; use tokio::sync::watch;
use hyper::{ use hyper::{
body::Incoming as IncomingBody, body::Incoming as IncomingBody,
header::{HeaderValue, HOST}, header::{HeaderValue, HOST, LOCATION},
Method, Request, Response, StatusCode, Method, Request, Response, StatusCode,
}; };
@@ -29,6 +29,7 @@ use garage_api_s3::error::{
CommonErrorDerivative, Error as ApiError, OkOrBadRequest, OkOrInternalError, CommonErrorDerivative, Error as ApiError, OkOrBadRequest, OkOrInternalError,
}; };
use garage_api_s3::get::{handle_get_without_ctx, handle_head_without_ctx}; 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; use garage_model::garage::Garage;
@@ -163,6 +164,8 @@ impl WebServer {
metrics_tags.push(KeyValue::new("host", host_header.clone())); metrics_tags.push(KeyValue::new("host", host_header.clone()));
} }
let req = req.map(|_| ());
// The actual handler // The actual handler
let res = self let res = self
.serve_file(&req) .serve_file(&req)
@@ -218,7 +221,7 @@ impl WebServer {
async fn serve_file( async fn serve_file(
self: &Arc<Self>, self: &Arc<Self>,
req: &Request<IncomingBody>, req: &Request<()>,
) -> Result<Response<BoxBody<ApiError>>, Error> { ) -> Result<Response<BoxBody<ApiError>>, Error> {
// Get http authority string (eg. [::1]:3902 or garage.tld:80) // Get http authority string (eg. [::1]:3902 or garage.tld:80)
let authority = req let authority = req
@@ -292,7 +295,15 @@ impl WebServer {
{ {
Ok(Response::builder() Ok(Response::builder()
.status(StatusCode::FOUND) .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()) .body(empty_body())
.unwrap()) .unwrap())
} }
@@ -322,7 +333,7 @@ impl WebServer {
// Create a fake HTTP request with path = the error document // Create a fake HTTP request with path = the error document
let req2 = Request::builder() let req2 = Request::builder()
.uri(format!("http://{}/{}", host, &error_document)) .uri(format!("http://{}/{}", host, &error_document))
.body(empty_body::<Infallible>()) .body(())
.unwrap(); .unwrap();
match handle_get_without_ctx( match handle_get_without_ctx(