mirror of
https://git.deuxfleurs.fr/Deuxfleurs/garage.git
synced 2024-11-21 23:51:00 +00:00
Merge pull request 'Dependency upgrades: http, hyper, aws-sdk, smaller deps' (#703) from dep-upgrade-202402 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/703
This commit is contained in:
commit
5d941e0100
59 changed files with 4736 additions and 3361 deletions
1916
Cargo.lock
generated
1916
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
102
Cargo.toml
102
Cargo.toml
|
@ -17,6 +17,8 @@ members = [
|
||||||
default-members = ["src/garage"]
|
default-members = ["src/garage"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
||||||
|
# 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 = { version = "0.9.1", path = "src/api" }
|
garage_api = { version = "0.9.1", path = "src/api" }
|
||||||
garage_block = { version = "0.9.1", path = "src/block" }
|
garage_block = { version = "0.9.1", path = "src/block" }
|
||||||
|
@ -28,6 +30,106 @@ garage_util = { version = "0.9.1", path = "src/util" }
|
||||||
garage_web = { version = "0.9.1", path = "src/web" }
|
garage_web = { version = "0.9.1", path = "src/web" }
|
||||||
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
||||||
|
|
||||||
|
# Netapp is a special sister crate
|
||||||
|
netapp = { version = "0.10", features = ["telemetry"] }
|
||||||
|
|
||||||
|
# External crates from crates.io
|
||||||
|
arc-swap = "1.0"
|
||||||
|
async-trait = "0.1.7"
|
||||||
|
backtrace = "0.3"
|
||||||
|
base64 = "0.21"
|
||||||
|
blake2 = "0.10"
|
||||||
|
bytes = "1.0"
|
||||||
|
bytesize = "1.1"
|
||||||
|
chrono = "0.4"
|
||||||
|
crypto-common = "0.1"
|
||||||
|
digest = "0.10"
|
||||||
|
err-derive = "0.3"
|
||||||
|
gethostname = "0.4"
|
||||||
|
git-version = "0.3.4"
|
||||||
|
hex = "0.4"
|
||||||
|
hexdump = "0.1"
|
||||||
|
hmac = "0.12"
|
||||||
|
idna = "0.5"
|
||||||
|
itertools = "0.12"
|
||||||
|
lazy_static = "1.4"
|
||||||
|
md-5 = "0.10"
|
||||||
|
mktemp = "0.5"
|
||||||
|
nix = { version = "0.27", default-features = false, features = ["fs"] }
|
||||||
|
nom = "7.1"
|
||||||
|
parse_duration = "2.1"
|
||||||
|
pin-project = "1.0.12"
|
||||||
|
pnet_datalink = "0.34"
|
||||||
|
rand = "0.8"
|
||||||
|
sha2 = "0.10"
|
||||||
|
sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" }
|
||||||
|
timeago = { version = "0.4", default-features = false }
|
||||||
|
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
|
||||||
|
|
||||||
|
clap = { version = "4.1", features = ["derive", "env"] }
|
||||||
|
pretty_env_logger = "0.5"
|
||||||
|
structopt = { version = "0.3", default-features = false }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
heed = { version = "0.11", default-features = false, features = ["lmdb"] }
|
||||||
|
rusqlite = "0.30.0"
|
||||||
|
sled = "0.34"
|
||||||
|
|
||||||
|
async-compression = { version = "0.4", features = ["tokio", "zstd"] }
|
||||||
|
zstd = { version = "0.13", default-features = false }
|
||||||
|
|
||||||
|
quick-xml = { version = "0.26", features = [ "serialize" ] }
|
||||||
|
rmp-serde = "1.1.2"
|
||||||
|
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
|
||||||
|
serde_bytes = "0.11"
|
||||||
|
serde_json = "1.0"
|
||||||
|
toml = "0.6"
|
||||||
|
|
||||||
|
# newer version requires rust edition 2021
|
||||||
|
k8s-openapi = { version = "0.16", features = ["v1_22"] }
|
||||||
|
kube = { version = "0.75", default-features = false, features = ["runtime", "derive", "client", "rustls-tls"] }
|
||||||
|
schemars = "0.8"
|
||||||
|
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-manual-roots", "json"] }
|
||||||
|
|
||||||
|
form_urlencoded = "1.0.0"
|
||||||
|
http = "1.0"
|
||||||
|
httpdate = "1.0"
|
||||||
|
http-range = "0.1"
|
||||||
|
http-body-util = "0.1"
|
||||||
|
hyper = { version = "1.0", features = ["server", "http1"] }
|
||||||
|
hyper-util = { version = "0.1", features = [ "full" ] }
|
||||||
|
multer = "3.0"
|
||||||
|
percent-encoding = "2.2"
|
||||||
|
roxmltree = "0.19"
|
||||||
|
url = "2.3"
|
||||||
|
|
||||||
|
futures = "0.3"
|
||||||
|
futures-util = "0.3"
|
||||||
|
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["io"] }
|
||||||
|
tokio-stream = { version = "0.1", features = ["net"] }
|
||||||
|
|
||||||
|
opentelemetry = { version = "0.17", features = [ "rt-tokio", "metrics", "trace" ] }
|
||||||
|
opentelemetry-prometheus = "0.10"
|
||||||
|
opentelemetry-otlp = "0.10"
|
||||||
|
prometheus = "0.13"
|
||||||
|
|
||||||
|
# used by the k2v-client crate only
|
||||||
|
aws-sigv4 = {version = "1.1" }
|
||||||
|
hyper-rustls = { version = "0.26", features = ["http2"] }
|
||||||
|
log = "0.4"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
# ---- used only as build / dev dependencies ----
|
||||||
|
assert-json-diff = "2.0"
|
||||||
|
rustc_version = "0.4.0"
|
||||||
|
static_init = "1.0"
|
||||||
|
|
||||||
|
aws-config = "1.1.4"
|
||||||
|
aws-sdk-config = "1.13"
|
||||||
|
aws-sdk-s3 = "1.14"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
lto = "off"
|
lto = "off"
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,8 @@
|
||||||
cargo2nixOverlay = cargo2nix.overlays.default;
|
cargo2nixOverlay = cargo2nix.overlays.default;
|
||||||
release = false;
|
release = false;
|
||||||
}).workspaceShell { packages = with pkgs; [
|
}).workspaceShell { packages = with pkgs; [
|
||||||
|
cargo-audit
|
||||||
|
cargo-outdated
|
||||||
rustfmt
|
rustfmt
|
||||||
clang
|
clang
|
||||||
mold
|
mold
|
||||||
|
|
|
@ -20,44 +20,46 @@ garage_block.workspace = true
|
||||||
garage_util.workspace = true
|
garage_util.workspace = true
|
||||||
garage_rpc.workspace = true
|
garage_rpc.workspace = true
|
||||||
|
|
||||||
async-trait = "0.1.7"
|
async-trait.workspace = true
|
||||||
base64 = "0.21"
|
base64.workspace = true
|
||||||
bytes = "1.0"
|
bytes.workspace = true
|
||||||
chrono = "0.4"
|
chrono.workspace = true
|
||||||
crypto-common = "0.1"
|
crypto-common.workspace = true
|
||||||
err-derive = "0.3"
|
err-derive.workspace = true
|
||||||
hex = "0.4"
|
hex.workspace = true
|
||||||
hmac = "0.12"
|
hmac.workspace = true
|
||||||
idna = "0.4"
|
idna.workspace = true
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
md-5 = "0.10"
|
md-5.workspace = true
|
||||||
nom = "7.1"
|
nom.workspace = true
|
||||||
sha2 = "0.10"
|
pin-project.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
|
||||||
futures = "0.3"
|
futures.workspace = true
|
||||||
futures-util = "0.3"
|
futures-util.workspace = true
|
||||||
pin-project = "1.0.12"
|
tokio.workspace = true
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio-stream.workspace = true
|
||||||
tokio-stream = "0.1"
|
|
||||||
|
|
||||||
form_urlencoded = "1.0.0"
|
form_urlencoded.workspace = true
|
||||||
http = "0.2"
|
http.workspace = true
|
||||||
httpdate = "1.0"
|
httpdate.workspace = true
|
||||||
http-range = "0.1"
|
http-range.workspace = true
|
||||||
hyper = { version = "0.14", features = ["server", "http1", "runtime", "tcp", "stream"] }
|
http-body-util.workspace = true
|
||||||
hyperlocal = { version = "0.8.0", default-features = false, features = ["server"] }
|
hyper.workspace = true
|
||||||
multer = "2.0"
|
hyper-util.workspace = true
|
||||||
percent-encoding = "2.1.0"
|
multer.workspace = true
|
||||||
roxmltree = "0.18"
|
percent-encoding.workspace = true
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
roxmltree.workspace = true
|
||||||
serde_bytes = "0.11"
|
url.workspace = true
|
||||||
serde_json = "1.0"
|
|
||||||
quick-xml = { version = "0.26", features = [ "serialize" ] }
|
|
||||||
url = "2.3"
|
|
||||||
|
|
||||||
opentelemetry = "0.17"
|
serde.workspace = true
|
||||||
opentelemetry-prometheus = { version = "0.10", optional = true }
|
serde_bytes.workspace = true
|
||||||
prometheus = { version = "0.13", optional = true }
|
serde_json.workspace = true
|
||||||
|
quick-xml.workspace = true
|
||||||
|
|
||||||
|
opentelemetry.workspace = true
|
||||||
|
opentelemetry-prometheus = { workspace = true, optional = true }
|
||||||
|
prometheus = { workspace = true, optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
|
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
|
||||||
|
|
|
@ -5,7 +5,7 @@ use async_trait::async_trait;
|
||||||
|
|
||||||
use futures::future::Future;
|
use futures::future::Future;
|
||||||
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
|
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
|
||||||
|
|
||||||
use opentelemetry::trace::SpanRef;
|
use opentelemetry::trace::SpanRef;
|
||||||
|
|
||||||
|
@ -27,7 +27,9 @@ use crate::admin::error::*;
|
||||||
use crate::admin::key::*;
|
use crate::admin::key::*;
|
||||||
use crate::admin::router_v0;
|
use crate::admin::router_v0;
|
||||||
use crate::admin::router_v1::{Authorization, Endpoint};
|
use crate::admin::router_v1::{Authorization, Endpoint};
|
||||||
use crate::helpers::host_to_bucket;
|
use crate::helpers::*;
|
||||||
|
|
||||||
|
pub type ResBody = BoxBody<Error>;
|
||||||
|
|
||||||
pub struct AdminApiServer {
|
pub struct AdminApiServer {
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
|
@ -71,16 +73,19 @@ impl AdminApiServer {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_options(&self, _req: &Request<Body>) -> Result<Response<Body>, Error> {
|
fn handle_options(&self, _req: &Request<IncomingBody>) -> Result<Response<ResBody>, Error> {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.header(ALLOW, "OPTIONS, GET, POST")
|
.header(ALLOW, "OPTIONS, GET, POST")
|
||||||
.header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST")
|
.header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST")
|
||||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_check_domain(&self, req: Request<Body>) -> Result<Response<Body>, Error> {
|
async fn handle_check_domain(
|
||||||
|
&self,
|
||||||
|
req: Request<IncomingBody>,
|
||||||
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let query_params: HashMap<String, String> = req
|
let query_params: HashMap<String, String> = req
|
||||||
.uri()
|
.uri()
|
||||||
.query()
|
.query()
|
||||||
|
@ -104,7 +109,7 @@ impl AdminApiServer {
|
||||||
if self.check_domain(domain).await? {
|
if self.check_domain(domain).await? {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::from(format!(
|
.body(string_body(format!(
|
||||||
"Domain '{domain}' is managed by Garage"
|
"Domain '{domain}' is managed by Garage"
|
||||||
)))?)
|
)))?)
|
||||||
} else {
|
} else {
|
||||||
|
@ -167,7 +172,7 @@ impl AdminApiServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_health(&self) -> Result<Response<Body>, Error> {
|
fn handle_health(&self) -> Result<Response<ResBody>, Error> {
|
||||||
let health = self.garage.system.health();
|
let health = self.garage.system.health();
|
||||||
|
|
||||||
let (status, status_str) = match health.status {
|
let (status, status_str) = match health.status {
|
||||||
|
@ -189,10 +194,10 @@ impl AdminApiServer {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(status)
|
.status(status)
|
||||||
.header(http::header::CONTENT_TYPE, "text/plain")
|
.header(http::header::CONTENT_TYPE, "text/plain")
|
||||||
.body(Body::from(status_str))?)
|
.body(string_body(status_str))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_metrics(&self) -> Result<Response<Body>, Error> {
|
fn handle_metrics(&self) -> Result<Response<ResBody>, Error> {
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
{
|
{
|
||||||
use opentelemetry::trace::Tracer;
|
use opentelemetry::trace::Tracer;
|
||||||
|
@ -212,7 +217,7 @@ impl AdminApiServer {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header(http::header::CONTENT_TYPE, encoder.format_type())
|
.header(http::header::CONTENT_TYPE, encoder.format_type())
|
||||||
.body(Body::from(buffer))?)
|
.body(bytes_body(buffer.into()))?)
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "metrics"))]
|
#[cfg(not(feature = "metrics"))]
|
||||||
Err(Error::bad_request(
|
Err(Error::bad_request(
|
||||||
|
@ -229,7 +234,7 @@ impl ApiHandler for AdminApiServer {
|
||||||
type Endpoint = Endpoint;
|
type Endpoint = Endpoint;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn parse_endpoint(&self, req: &Request<Body>) -> Result<Endpoint, Error> {
|
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<Endpoint, Error> {
|
||||||
if req.uri().path().starts_with("/v0/") {
|
if req.uri().path().starts_with("/v0/") {
|
||||||
let endpoint_v0 = router_v0::Endpoint::from_request(req)?;
|
let endpoint_v0 = router_v0::Endpoint::from_request(req)?;
|
||||||
Endpoint::from_v0(endpoint_v0)
|
Endpoint::from_v0(endpoint_v0)
|
||||||
|
@ -240,9 +245,9 @@ impl ApiHandler for AdminApiServer {
|
||||||
|
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
endpoint: Endpoint,
|
endpoint: Endpoint,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let expected_auth_header =
|
let expected_auth_header =
|
||||||
match endpoint.authorization_type() {
|
match endpoint.authorization_type() {
|
||||||
Authorization::None => None,
|
Authorization::None => None,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use garage_util::crdt::*;
|
use garage_util::crdt::*;
|
||||||
|
@ -17,12 +17,13 @@ use garage_model::permission::*;
|
||||||
use garage_model::s3::mpu_table;
|
use garage_model::s3::mpu_table;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
|
|
||||||
|
use crate::admin::api_server::ResBody;
|
||||||
use crate::admin::error::*;
|
use crate::admin::error::*;
|
||||||
use crate::admin::key::ApiBucketKeyPerm;
|
use crate::admin::key::ApiBucketKeyPerm;
|
||||||
use crate::common_error::CommonError;
|
use crate::common_error::CommonError;
|
||||||
use crate::helpers::{json_ok_response, parse_json_body};
|
use crate::helpers::*;
|
||||||
|
|
||||||
pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||||
let buckets = garage
|
let buckets = garage
|
||||||
.bucket_table
|
.bucket_table
|
||||||
.get_range(
|
.get_range(
|
||||||
|
@ -90,7 +91,7 @@ pub async fn handle_get_bucket_info(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
id: Option<String>,
|
id: Option<String>,
|
||||||
global_alias: Option<String>,
|
global_alias: Option<String>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let bucket_id = match (id, global_alias) {
|
let bucket_id = match (id, global_alias) {
|
||||||
(Some(id), None) => parse_bucket_id(&id)?,
|
(Some(id), None) => parse_bucket_id(&id)?,
|
||||||
(None, Some(ga)) => garage
|
(None, Some(ga)) => garage
|
||||||
|
@ -111,7 +112,7 @@ pub async fn handle_get_bucket_info(
|
||||||
async fn bucket_info_results(
|
async fn bucket_info_results(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let bucket = garage
|
let bucket = garage
|
||||||
.bucket_helper()
|
.bucket_helper()
|
||||||
.get_existing_bucket(bucket_id)
|
.get_existing_bucket(bucket_id)
|
||||||
|
@ -268,9 +269,9 @@ struct GetBucketInfoKey {
|
||||||
|
|
||||||
pub async fn handle_create_bucket(
|
pub async fn handle_create_bucket(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let req = parse_json_body::<CreateBucketRequest>(req).await?;
|
let req = parse_json_body::<CreateBucketRequest, _, Error>(req).await?;
|
||||||
|
|
||||||
if let Some(ga) = &req.global_alias {
|
if let Some(ga) = &req.global_alias {
|
||||||
if !is_valid_bucket_name(ga) {
|
if !is_valid_bucket_name(ga) {
|
||||||
|
@ -360,7 +361,7 @@ struct CreateBucketLocalAlias {
|
||||||
pub async fn handle_delete_bucket(
|
pub async fn handle_delete_bucket(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
id: String,
|
id: String,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let helper = garage.bucket_helper();
|
let helper = garage.bucket_helper();
|
||||||
|
|
||||||
let bucket_id = parse_bucket_id(&id)?;
|
let bucket_id = parse_bucket_id(&id)?;
|
||||||
|
@ -403,15 +404,15 @@ pub async fn handle_delete_bucket(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_update_bucket(
|
pub async fn handle_update_bucket(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
id: String,
|
id: String,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let req = parse_json_body::<UpdateBucketRequest>(req).await?;
|
let req = parse_json_body::<UpdateBucketRequest, _, Error>(req).await?;
|
||||||
let bucket_id = parse_bucket_id(&id)?;
|
let bucket_id = parse_bucket_id(&id)?;
|
||||||
|
|
||||||
let mut bucket = garage
|
let mut bucket = garage
|
||||||
|
@ -470,10 +471,10 @@ struct UpdateBucketWebsiteAccess {
|
||||||
|
|
||||||
pub async fn handle_bucket_change_key_perm(
|
pub async fn handle_bucket_change_key_perm(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
new_perm_flag: bool,
|
new_perm_flag: bool,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let req = parse_json_body::<BucketKeyPermChangeRequest>(req).await?;
|
let req = parse_json_body::<BucketKeyPermChangeRequest, _, Error>(req).await?;
|
||||||
|
|
||||||
let bucket_id = parse_bucket_id(&req.bucket_id)?;
|
let bucket_id = parse_bucket_id(&req.bucket_id)?;
|
||||||
|
|
||||||
|
@ -526,7 +527,7 @@ pub async fn handle_global_alias_bucket(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
bucket_id: String,
|
bucket_id: String,
|
||||||
alias: String,
|
alias: String,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let bucket_id = parse_bucket_id(&bucket_id)?;
|
let bucket_id = parse_bucket_id(&bucket_id)?;
|
||||||
|
|
||||||
garage
|
garage
|
||||||
|
@ -541,7 +542,7 @@ pub async fn handle_global_unalias_bucket(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
bucket_id: String,
|
bucket_id: String,
|
||||||
alias: String,
|
alias: String,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let bucket_id = parse_bucket_id(&bucket_id)?;
|
let bucket_id = parse_bucket_id(&bucket_id)?;
|
||||||
|
|
||||||
garage
|
garage
|
||||||
|
@ -557,7 +558,7 @@ pub async fn handle_local_alias_bucket(
|
||||||
bucket_id: String,
|
bucket_id: String,
|
||||||
access_key_id: String,
|
access_key_id: String,
|
||||||
alias: String,
|
alias: String,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let bucket_id = parse_bucket_id(&bucket_id)?;
|
let bucket_id = parse_bucket_id(&bucket_id)?;
|
||||||
|
|
||||||
garage
|
garage
|
||||||
|
@ -573,7 +574,7 @@ pub async fn handle_local_unalias_bucket(
|
||||||
bucket_id: String,
|
bucket_id: String,
|
||||||
access_key_id: String,
|
access_key_id: String,
|
||||||
alias: String,
|
alias: String,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let bucket_id = parse_bucket_id(&bucket_id)?;
|
let bucket_id = parse_bucket_id(&bucket_id)?;
|
||||||
|
|
||||||
garage
|
garage
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{body::Incoming as IncomingBody, Request, Response};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use garage_util::crdt::*;
|
use garage_util::crdt::*;
|
||||||
|
@ -11,10 +11,11 @@ use garage_rpc::layout;
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
|
|
||||||
|
use crate::admin::api_server::ResBody;
|
||||||
use crate::admin::error::*;
|
use crate::admin::error::*;
|
||||||
use crate::helpers::{json_ok_response, parse_json_body};
|
use crate::helpers::{json_ok_response, parse_json_body};
|
||||||
|
|
||||||
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||||
let res = GetClusterStatusResponse {
|
let res = GetClusterStatusResponse {
|
||||||
node: hex::encode(garage.system.id),
|
node: hex::encode(garage.system.id),
|
||||||
garage_version: garage_util::version::garage_version(),
|
garage_version: garage_util::version::garage_version(),
|
||||||
|
@ -39,7 +40,7 @@ pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<
|
||||||
Ok(json_ok_response(&res)?)
|
Ok(json_ok_response(&res)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||||
use garage_rpc::system::ClusterHealthStatus;
|
use garage_rpc::system::ClusterHealthStatus;
|
||||||
let health = garage.system.health();
|
let health = garage.system.health();
|
||||||
let health = ClusterHealth {
|
let health = ClusterHealth {
|
||||||
|
@ -61,9 +62,9 @@ pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<
|
||||||
|
|
||||||
pub async fn handle_connect_cluster_nodes(
|
pub async fn handle_connect_cluster_nodes(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let req = parse_json_body::<Vec<String>>(req).await?;
|
let req = parse_json_body::<Vec<String>, _, Error>(req).await?;
|
||||||
|
|
||||||
let res = futures::future::join_all(req.iter().map(|node| garage.system.connect(node)))
|
let res = futures::future::join_all(req.iter().map(|node| garage.system.connect(node)))
|
||||||
.await
|
.await
|
||||||
|
@ -83,7 +84,7 @@ pub async fn handle_connect_cluster_nodes(
|
||||||
Ok(json_ok_response(&res)?)
|
Ok(json_ok_response(&res)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_get_cluster_layout(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
pub async fn handle_get_cluster_layout(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||||
let res = format_cluster_layout(&garage.system.get_cluster_layout());
|
let res = format_cluster_layout(&garage.system.get_cluster_layout());
|
||||||
|
|
||||||
Ok(json_ok_response(&res)?)
|
Ok(json_ok_response(&res)?)
|
||||||
|
@ -203,9 +204,9 @@ struct KnownNodeResp {
|
||||||
|
|
||||||
pub async fn handle_update_cluster_layout(
|
pub async fn handle_update_cluster_layout(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let updates = parse_json_body::<UpdateClusterLayoutRequest>(req).await?;
|
let updates = parse_json_body::<UpdateClusterLayoutRequest, _, Error>(req).await?;
|
||||||
|
|
||||||
let mut layout = garage.system.get_cluster_layout();
|
let mut layout = garage.system.get_cluster_layout();
|
||||||
|
|
||||||
|
@ -243,9 +244,9 @@ pub async fn handle_update_cluster_layout(
|
||||||
|
|
||||||
pub async fn handle_apply_cluster_layout(
|
pub async fn handle_apply_cluster_layout(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let param = parse_json_body::<ApplyRevertLayoutRequest>(req).await?;
|
let param = parse_json_body::<ApplyRevertLayoutRequest, _, Error>(req).await?;
|
||||||
|
|
||||||
let layout = garage.system.get_cluster_layout();
|
let layout = garage.system.get_cluster_layout();
|
||||||
let (layout, msg) = layout.apply_staged_changes(Some(param.version))?;
|
let (layout, msg) = layout.apply_staged_changes(Some(param.version))?;
|
||||||
|
@ -261,9 +262,9 @@ pub async fn handle_apply_cluster_layout(
|
||||||
|
|
||||||
pub async fn handle_revert_cluster_layout(
|
pub async fn handle_revert_cluster_layout(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let param = parse_json_body::<ApplyRevertLayoutRequest>(req).await?;
|
let param = parse_json_body::<ApplyRevertLayoutRequest, _, Error>(req).await?;
|
||||||
|
|
||||||
let layout = garage.system.get_cluster_layout();
|
let layout = garage.system.get_cluster_layout();
|
||||||
let layout = layout.revert_staged_changes(Some(param.version))?;
|
let layout = layout.revert_staged_changes(Some(param.version))?;
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use err_derive::Error;
|
use err_derive::Error;
|
||||||
use hyper::header::HeaderValue;
|
use hyper::header::HeaderValue;
|
||||||
use hyper::{Body, HeaderMap, StatusCode};
|
use hyper::{HeaderMap, StatusCode};
|
||||||
|
|
||||||
pub use garage_model::helper::error::Error as HelperError;
|
pub use garage_model::helper::error::Error as HelperError;
|
||||||
|
|
||||||
use crate::common_error::CommonError;
|
use crate::common_error::CommonError;
|
||||||
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
||||||
use crate::generic_server::ApiError;
|
use crate::generic_server::ApiError;
|
||||||
use crate::helpers::CustomApiErrorBody;
|
use crate::helpers::*;
|
||||||
|
|
||||||
/// Errors of this crate
|
/// Errors of this crate
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -40,18 +40,6 @@ where
|
||||||
|
|
||||||
impl CommonErrorDerivative for Error {}
|
impl CommonErrorDerivative for Error {}
|
||||||
|
|
||||||
impl From<HelperError> for Error {
|
|
||||||
fn from(err: HelperError) -> Self {
|
|
||||||
match err {
|
|
||||||
HelperError::Internal(i) => Self::Common(CommonError::InternalError(i)),
|
|
||||||
HelperError::BadRequest(b) => Self::Common(CommonError::BadRequest(b)),
|
|
||||||
HelperError::InvalidBucketName(n) => Self::Common(CommonError::InvalidBucketName(n)),
|
|
||||||
HelperError::NoSuchBucket(n) => Self::Common(CommonError::NoSuchBucket(n)),
|
|
||||||
HelperError::NoSuchAccessKey(n) => Self::NoSuchAccessKey(n),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
fn code(&self) -> &'static str {
|
fn code(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
@ -77,14 +65,14 @@ impl ApiError for Error {
|
||||||
header_map.append(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
header_map.append(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn http_body(&self, garage_region: &str, path: &str) -> Body {
|
fn http_body(&self, garage_region: &str, path: &str) -> ErrorBody {
|
||||||
let error = CustomApiErrorBody {
|
let error = CustomApiErrorBody {
|
||||||
code: self.code().to_string(),
|
code: self.code().to_string(),
|
||||||
message: format!("{}", self),
|
message: format!("{}", self),
|
||||||
path: path.to_string(),
|
path: path.to_string(),
|
||||||
region: garage_region.to_string(),
|
region: garage_region.to_string(),
|
||||||
};
|
};
|
||||||
Body::from(serde_json::to_string_pretty(&error).unwrap_or_else(|_| {
|
let error_str = serde_json::to_string_pretty(&error).unwrap_or_else(|_| {
|
||||||
r#"
|
r#"
|
||||||
{
|
{
|
||||||
"code": "InternalError",
|
"code": "InternalError",
|
||||||
|
@ -92,6 +80,7 @@ impl ApiError for Error {
|
||||||
}
|
}
|
||||||
"#
|
"#
|
||||||
.into()
|
.into()
|
||||||
}))
|
});
|
||||||
|
error_body(error_str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use garage_table::*;
|
use garage_table::*;
|
||||||
|
@ -9,10 +9,11 @@ use garage_table::*;
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_model::key_table::*;
|
use garage_model::key_table::*;
|
||||||
|
|
||||||
|
use crate::admin::api_server::ResBody;
|
||||||
use crate::admin::error::*;
|
use crate::admin::error::*;
|
||||||
use crate::helpers::{is_default, json_ok_response, parse_json_body};
|
use crate::helpers::*;
|
||||||
|
|
||||||
pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||||
let res = garage
|
let res = garage
|
||||||
.key_table
|
.key_table
|
||||||
.get_range(
|
.get_range(
|
||||||
|
@ -45,7 +46,7 @@ pub async fn handle_get_key_info(
|
||||||
id: Option<String>,
|
id: Option<String>,
|
||||||
search: Option<String>,
|
search: Option<String>,
|
||||||
show_secret_key: bool,
|
show_secret_key: bool,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let key = if let Some(id) = id {
|
let key = if let Some(id) = id {
|
||||||
garage.key_helper().get_existing_key(&id).await?
|
garage.key_helper().get_existing_key(&id).await?
|
||||||
} else if let Some(search) = search {
|
} else if let Some(search) = search {
|
||||||
|
@ -62,9 +63,9 @@ pub async fn handle_get_key_info(
|
||||||
|
|
||||||
pub async fn handle_create_key(
|
pub async fn handle_create_key(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let req = parse_json_body::<CreateKeyRequest>(req).await?;
|
let req = parse_json_body::<CreateKeyRequest, _, Error>(req).await?;
|
||||||
|
|
||||||
let key = Key::new(req.name.as_deref().unwrap_or("Unnamed key"));
|
let key = Key::new(req.name.as_deref().unwrap_or("Unnamed key"));
|
||||||
garage.key_table.insert(&key).await?;
|
garage.key_table.insert(&key).await?;
|
||||||
|
@ -80,9 +81,9 @@ struct CreateKeyRequest {
|
||||||
|
|
||||||
pub async fn handle_import_key(
|
pub async fn handle_import_key(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let req = parse_json_body::<ImportKeyRequest>(req).await?;
|
let req = parse_json_body::<ImportKeyRequest, _, Error>(req).await?;
|
||||||
|
|
||||||
let prev_key = garage.key_table.get(&EmptyKey, &req.access_key_id).await?;
|
let prev_key = garage.key_table.get(&EmptyKey, &req.access_key_id).await?;
|
||||||
if prev_key.is_some() {
|
if prev_key.is_some() {
|
||||||
|
@ -111,9 +112,9 @@ struct ImportKeyRequest {
|
||||||
pub async fn handle_update_key(
|
pub async fn handle_update_key(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
id: String,
|
id: String,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let req = parse_json_body::<UpdateKeyRequest>(req).await?;
|
let req = parse_json_body::<UpdateKeyRequest, _, Error>(req).await?;
|
||||||
|
|
||||||
let mut key = garage.key_helper().get_existing_key(&id).await?;
|
let mut key = garage.key_helper().get_existing_key(&id).await?;
|
||||||
|
|
||||||
|
@ -146,7 +147,10 @@ struct UpdateKeyRequest {
|
||||||
deny: Option<KeyPerm>,
|
deny: Option<KeyPerm>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_delete_key(garage: &Arc<Garage>, id: String) -> Result<Response<Body>, Error> {
|
pub async fn handle_delete_key(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let mut key = garage.key_helper().get_existing_key(&id).await?;
|
let mut key = garage.key_helper().get_existing_key(&id).await?;
|
||||||
|
|
||||||
key.state.as_option().unwrap();
|
key.state.as_option().unwrap();
|
||||||
|
@ -155,14 +159,14 @@ pub async fn handle_delete_key(garage: &Arc<Garage>, id: String) -> Result<Respo
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn key_info_results(
|
async fn key_info_results(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
key: Key,
|
key: Key,
|
||||||
show_secret: bool,
|
show_secret: bool,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let mut relevant_buckets = HashMap::new();
|
let mut relevant_buckets = HashMap::new();
|
||||||
|
|
||||||
let key_state = key.state.as_option().unwrap();
|
let key_state = key.state.as_option().unwrap();
|
||||||
|
|
|
@ -3,6 +3,8 @@ use hyper::StatusCode;
|
||||||
|
|
||||||
use garage_util::error::Error as GarageError;
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
|
use garage_model::helper::error::Error as HelperError;
|
||||||
|
|
||||||
/// Errors of this crate
|
/// Errors of this crate
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum CommonError {
|
pub enum CommonError {
|
||||||
|
@ -28,6 +30,10 @@ pub enum CommonError {
|
||||||
#[error(display = "Bad request: {}", _0)]
|
#[error(display = "Bad request: {}", _0)]
|
||||||
BadRequest(String),
|
BadRequest(String),
|
||||||
|
|
||||||
|
/// The client sent a header with invalid value
|
||||||
|
#[error(display = "Invalid header value: {}", _0)]
|
||||||
|
InvalidHeader(#[error(source)] hyper::header::ToStrError),
|
||||||
|
|
||||||
// ---- SPECIFIC ERROR CONDITIONS ----
|
// ---- SPECIFIC ERROR CONDITIONS ----
|
||||||
// These have to be error codes referenced in the S3 spec here:
|
// These have to be error codes referenced in the S3 spec here:
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
|
||||||
|
@ -64,7 +70,9 @@ impl CommonError {
|
||||||
CommonError::Forbidden(_) => StatusCode::FORBIDDEN,
|
CommonError::Forbidden(_) => StatusCode::FORBIDDEN,
|
||||||
CommonError::NoSuchBucket(_) => StatusCode::NOT_FOUND,
|
CommonError::NoSuchBucket(_) => StatusCode::NOT_FOUND,
|
||||||
CommonError::BucketNotEmpty | CommonError::BucketAlreadyExists => StatusCode::CONFLICT,
|
CommonError::BucketNotEmpty | CommonError::BucketAlreadyExists => StatusCode::CONFLICT,
|
||||||
CommonError::InvalidBucketName(_) => StatusCode::BAD_REQUEST,
|
CommonError::InvalidBucketName(_) | CommonError::InvalidHeader(_) => {
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +92,7 @@ impl CommonError {
|
||||||
CommonError::BucketAlreadyExists => "BucketAlreadyExists",
|
CommonError::BucketAlreadyExists => "BucketAlreadyExists",
|
||||||
CommonError::BucketNotEmpty => "BucketNotEmpty",
|
CommonError::BucketNotEmpty => "BucketNotEmpty",
|
||||||
CommonError::InvalidBucketName(_) => "InvalidBucketName",
|
CommonError::InvalidBucketName(_) => "InvalidBucketName",
|
||||||
|
CommonError::InvalidHeader(_) => "InvalidHeaderValue",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +101,18 @@ impl CommonError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<HelperError> for CommonError {
|
||||||
|
fn from(err: HelperError) -> Self {
|
||||||
|
match err {
|
||||||
|
HelperError::Internal(i) => Self::InternalError(i),
|
||||||
|
HelperError::BadRequest(b) => Self::BadRequest(b),
|
||||||
|
HelperError::InvalidBucketName(n) => Self::InvalidBucketName(n),
|
||||||
|
HelperError::NoSuchBucket(n) => Self::NoSuchBucket(n),
|
||||||
|
e => Self::bad_request(format!("{}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait CommonErrorDerivative: From<CommonError> {
|
pub trait CommonErrorDerivative: From<CommonError> {
|
||||||
fn internal_error<M: ToString>(msg: M) -> Self {
|
fn internal_error<M: ToString>(msg: M) -> Self {
|
||||||
Self::from(CommonError::InternalError(GarageError::Message(
|
Self::from(CommonError::InternalError(GarageError::Message(
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::convert::Infallible;
|
||||||
use std::fs::{self, Permissions};
|
use std::fs::{self, Permissions};
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -5,16 +6,18 @@ use std::sync::Arc;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use futures::future::Future;
|
use futures::future::Future;
|
||||||
|
use futures::stream::{futures_unordered::FuturesUnordered, StreamExt};
|
||||||
|
|
||||||
|
use http_body_util::BodyExt;
|
||||||
use hyper::header::HeaderValue;
|
use hyper::header::HeaderValue;
|
||||||
use hyper::server::conn::AddrStream;
|
use hyper::server::conn::http1;
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::service_fn;
|
||||||
use hyper::{Body, Request, Response, Server};
|
use hyper::{body::Incoming as IncomingBody, Request, Response};
|
||||||
use hyper::{HeaderMap, StatusCode};
|
use hyper::{HeaderMap, StatusCode};
|
||||||
|
use hyper_util::rt::TokioIo;
|
||||||
|
|
||||||
use hyperlocal::UnixServerExt;
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
|
use tokio::net::{TcpListener, TcpStream, UnixListener, UnixStream};
|
||||||
use tokio::net::UnixStream;
|
|
||||||
|
|
||||||
use opentelemetry::{
|
use opentelemetry::{
|
||||||
global,
|
global,
|
||||||
|
@ -28,6 +31,8 @@ use garage_util::forwarded_headers;
|
||||||
use garage_util::metrics::{gen_trace_id, RecordDuration};
|
use garage_util::metrics::{gen_trace_id, RecordDuration};
|
||||||
use garage_util::socket_address::UnixOrTCPSocketAddress;
|
use garage_util::socket_address::UnixOrTCPSocketAddress;
|
||||||
|
|
||||||
|
use crate::helpers::{BoxBody, ErrorBody};
|
||||||
|
|
||||||
pub(crate) trait ApiEndpoint: Send + Sync + 'static {
|
pub(crate) trait ApiEndpoint: Send + Sync + 'static {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
fn add_span_attributes(&self, span: SpanRef<'_>);
|
fn add_span_attributes(&self, span: SpanRef<'_>);
|
||||||
|
@ -36,7 +41,7 @@ pub(crate) trait ApiEndpoint: Send + Sync + 'static {
|
||||||
pub trait ApiError: std::error::Error + Send + Sync + 'static {
|
pub trait ApiError: std::error::Error + Send + Sync + 'static {
|
||||||
fn http_status_code(&self) -> StatusCode;
|
fn http_status_code(&self) -> StatusCode;
|
||||||
fn add_http_headers(&self, header_map: &mut HeaderMap<HeaderValue>);
|
fn add_http_headers(&self, header_map: &mut HeaderMap<HeaderValue>);
|
||||||
fn http_body(&self, garage_region: &str, path: &str) -> Body;
|
fn http_body(&self, garage_region: &str, path: &str) -> ErrorBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -47,12 +52,12 @@ pub(crate) trait ApiHandler: Send + Sync + 'static {
|
||||||
type Endpoint: ApiEndpoint;
|
type Endpoint: ApiEndpoint;
|
||||||
type Error: ApiError;
|
type Error: ApiError;
|
||||||
|
|
||||||
fn parse_endpoint(&self, r: &Request<Body>) -> Result<Self::Endpoint, Self::Error>;
|
fn parse_endpoint(&self, r: &Request<IncomingBody>) -> Result<Self::Endpoint, Self::Error>;
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
endpoint: Self::Endpoint,
|
endpoint: Self::Endpoint,
|
||||||
) -> Result<Response<Body>, Self::Error>;
|
) -> Result<Response<BoxBody<Self::Error>>, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ApiServer<A: ApiHandler> {
|
pub(crate) struct ApiServer<A: ApiHandler> {
|
||||||
|
@ -101,32 +106,6 @@ impl<A: ApiHandler> ApiServer<A> {
|
||||||
unix_bind_addr_mode: Option<u32>,
|
unix_bind_addr_mode: Option<u32>,
|
||||||
shutdown_signal: impl Future<Output = ()>,
|
shutdown_signal: impl Future<Output = ()>,
|
||||||
) -> Result<(), GarageError> {
|
) -> Result<(), GarageError> {
|
||||||
let tcp_service = make_service_fn(|conn: &AddrStream| {
|
|
||||||
let this = self.clone();
|
|
||||||
|
|
||||||
let client_addr = conn.remote_addr();
|
|
||||||
async move {
|
|
||||||
Ok::<_, GarageError>(service_fn(move |req: Request<Body>| {
|
|
||||||
let this = this.clone();
|
|
||||||
|
|
||||||
this.handler(req, client_addr.to_string())
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let unix_service = make_service_fn(|_: &UnixStream| {
|
|
||||||
let this = self.clone();
|
|
||||||
|
|
||||||
let path = bind_addr.to_string();
|
|
||||||
async move {
|
|
||||||
Ok::<_, GarageError>(service_fn(move |req: Request<Body>| {
|
|
||||||
let this = this.clone();
|
|
||||||
|
|
||||||
this.handler(req, path.clone())
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"{} API server listening on {}",
|
"{} API server listening on {}",
|
||||||
A::API_NAME_DISPLAY,
|
A::API_NAME_DISPLAY,
|
||||||
|
@ -135,38 +114,35 @@ impl<A: ApiHandler> ApiServer<A> {
|
||||||
|
|
||||||
match bind_addr {
|
match bind_addr {
|
||||||
UnixOrTCPSocketAddress::TCPSocket(addr) => {
|
UnixOrTCPSocketAddress::TCPSocket(addr) => {
|
||||||
Server::bind(&addr)
|
let listener = TcpListener::bind(addr).await?;
|
||||||
.serve(tcp_service)
|
|
||||||
.with_graceful_shutdown(shutdown_signal)
|
let handler = move |request, socketaddr| self.clone().handler(request, socketaddr);
|
||||||
.await?
|
server_loop(listener, handler, shutdown_signal).await
|
||||||
}
|
}
|
||||||
UnixOrTCPSocketAddress::UnixSocket(ref path) => {
|
UnixOrTCPSocketAddress::UnixSocket(ref path) => {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
fs::remove_file(path)?
|
fs::remove_file(path)?
|
||||||
}
|
}
|
||||||
|
|
||||||
let bound = Server::bind_unix(path)?;
|
let listener = UnixListener::bind(path)?;
|
||||||
|
let listener = UnixListenerOn(listener, path.display().to_string());
|
||||||
|
|
||||||
fs::set_permissions(
|
fs::set_permissions(
|
||||||
path,
|
path,
|
||||||
Permissions::from_mode(unix_bind_addr_mode.unwrap_or(0o222)),
|
Permissions::from_mode(unix_bind_addr_mode.unwrap_or(0o222)),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
bound
|
let handler = move |request, socketaddr| self.clone().handler(request, socketaddr);
|
||||||
.serve(unix_service)
|
server_loop(listener, handler, shutdown_signal).await
|
||||||
.with_graceful_shutdown(shutdown_signal)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handler(
|
async fn handler(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
addr: String,
|
addr: String,
|
||||||
) -> Result<Response<Body>, GarageError> {
|
) -> Result<Response<BoxBody<A::Error>>, http::Error> {
|
||||||
let uri = req.uri().clone();
|
let uri = req.uri().clone();
|
||||||
|
|
||||||
if let Ok(forwarded_for_ip_addr) =
|
if let Ok(forwarded_for_ip_addr) =
|
||||||
|
@ -205,7 +181,7 @@ impl<A: ApiHandler> ApiServer<A> {
|
||||||
Ok(x)
|
Ok(x)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let body: Body = e.http_body(&self.region, uri.path());
|
let body = e.http_body(&self.region, uri.path());
|
||||||
let mut http_error_builder = Response::builder().status(e.http_status_code());
|
let mut http_error_builder = Response::builder().status(e.http_status_code());
|
||||||
|
|
||||||
if let Some(header_map) = http_error_builder.headers_mut() {
|
if let Some(header_map) = http_error_builder.headers_mut() {
|
||||||
|
@ -219,12 +195,16 @@ impl<A: ApiHandler> ApiServer<A> {
|
||||||
} else {
|
} else {
|
||||||
info!("Response: error {}, {}", e.http_status_code(), e);
|
info!("Response: error {}, {}", e.http_status_code(), e);
|
||||||
}
|
}
|
||||||
Ok(http_error)
|
Ok(http_error
|
||||||
|
.map(|body| BoxBody::new(body.map_err(|_: Infallible| unreachable!()))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handler_stage2(&self, req: Request<Body>) -> Result<Response<Body>, A::Error> {
|
async fn handler_stage2(
|
||||||
|
&self,
|
||||||
|
req: Request<IncomingBody>,
|
||||||
|
) -> Result<Response<BoxBody<A::Error>>, A::Error> {
|
||||||
let endpoint = self.api_handler.parse_endpoint(&req)?;
|
let endpoint = self.api_handler.parse_endpoint(&req)?;
|
||||||
debug!("Endpoint: {}", endpoint.name());
|
debug!("Endpoint: {}", endpoint.name());
|
||||||
|
|
||||||
|
@ -265,3 +245,105 @@ impl<A: ApiHandler> ApiServer<A> {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==== helper functions ====
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Accept: Send + Sync + 'static {
|
||||||
|
type Stream: AsyncRead + AsyncWrite + Send + Sync + 'static;
|
||||||
|
async fn accept(&self) -> std::io::Result<(Self::Stream, String)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Accept for TcpListener {
|
||||||
|
type Stream = TcpStream;
|
||||||
|
async fn accept(&self) -> std::io::Result<(Self::Stream, String)> {
|
||||||
|
self.accept()
|
||||||
|
.await
|
||||||
|
.map(|(stream, addr)| (stream, addr.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UnixListenerOn(pub UnixListener, pub String);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Accept for UnixListenerOn {
|
||||||
|
type Stream = UnixStream;
|
||||||
|
async fn accept(&self) -> std::io::Result<(Self::Stream, String)> {
|
||||||
|
self.0
|
||||||
|
.accept()
|
||||||
|
.await
|
||||||
|
.map(|(stream, _addr)| (stream, self.1.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn server_loop<A, H, F, E>(
|
||||||
|
listener: A,
|
||||||
|
handler: H,
|
||||||
|
shutdown_signal: impl Future<Output = ()>,
|
||||||
|
) -> Result<(), GarageError>
|
||||||
|
where
|
||||||
|
A: Accept,
|
||||||
|
H: Fn(Request<IncomingBody>, String) -> F + Send + Sync + Clone + 'static,
|
||||||
|
F: Future<Output = Result<Response<BoxBody<E>>, http::Error>> + Send + 'static,
|
||||||
|
E: Send + Sync + std::error::Error + 'static,
|
||||||
|
{
|
||||||
|
tokio::pin!(shutdown_signal);
|
||||||
|
|
||||||
|
let (conn_in, mut conn_out) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
let connection_collector = tokio::spawn(async move {
|
||||||
|
let mut collection = FuturesUnordered::new();
|
||||||
|
loop {
|
||||||
|
let collect_next = async {
|
||||||
|
if collection.is_empty() {
|
||||||
|
futures::future::pending().await
|
||||||
|
} else {
|
||||||
|
collection.next().await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tokio::select! {
|
||||||
|
result = collect_next => {
|
||||||
|
trace!("HTTP connection finished: {:?}", result);
|
||||||
|
}
|
||||||
|
new_fut = conn_out.recv() => {
|
||||||
|
match new_fut {
|
||||||
|
Some(f) => collection.push(f),
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("Collecting last open HTTP connections.");
|
||||||
|
while let Some(conn_res) = collection.next().await {
|
||||||
|
trace!("HTTP connection finished: {:?}", conn_res);
|
||||||
|
}
|
||||||
|
debug!("No more HTTP connections to collect");
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (stream, client_addr) = tokio::select! {
|
||||||
|
acc = listener.accept() => acc?,
|
||||||
|
_ = &mut shutdown_signal => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
let io = TokioIo::new(stream);
|
||||||
|
|
||||||
|
let handler = handler.clone();
|
||||||
|
let serve = move |req: Request<IncomingBody>| handler(req, client_addr.clone());
|
||||||
|
|
||||||
|
let fut = tokio::task::spawn(async move {
|
||||||
|
let io = Box::pin(io);
|
||||||
|
if let Err(e) = http1::Builder::new()
|
||||||
|
.serve_connection(io, service_fn(serve))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
debug!("Error handling HTTP connection: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
conn_in.send(fut)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
connection_collector.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
use hyper::{Body, Request, Response};
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use futures::{Stream, StreamExt, TryStreamExt};
|
||||||
|
|
||||||
|
use http_body_util::{BodyExt, Full as FullBody};
|
||||||
|
use hyper::{
|
||||||
|
body::{Body, Bytes},
|
||||||
|
Request, Response,
|
||||||
|
};
|
||||||
use idna::domain_to_unicode;
|
use idna::domain_to_unicode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
use crate::common_error::{CommonError as Error, *};
|
use crate::common_error::{CommonError as Error, *};
|
||||||
|
|
||||||
/// What kind of authorization is required to perform a given action
|
/// What kind of authorization is required to perform a given action
|
||||||
|
@ -138,18 +148,64 @@ pub fn key_after_prefix(pfx: &str) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn parse_json_body<T: for<'de> Deserialize<'de>>(req: Request<Body>) -> Result<T, Error> {
|
// =============== body helpers =================
|
||||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
|
||||||
|
pub type EmptyBody = http_body_util::Empty<bytes::Bytes>;
|
||||||
|
pub type ErrorBody = FullBody<bytes::Bytes>;
|
||||||
|
pub type BoxBody<E> = http_body_util::combinators::BoxBody<bytes::Bytes, E>;
|
||||||
|
|
||||||
|
pub fn string_body<E>(s: String) -> BoxBody<E> {
|
||||||
|
bytes_body(bytes::Bytes::from(s.into_bytes()))
|
||||||
|
}
|
||||||
|
pub fn bytes_body<E>(b: bytes::Bytes) -> BoxBody<E> {
|
||||||
|
BoxBody::new(FullBody::new(b).map_err(|_: Infallible| unreachable!()))
|
||||||
|
}
|
||||||
|
pub fn empty_body<E>() -> BoxBody<E> {
|
||||||
|
BoxBody::new(http_body_util::Empty::new().map_err(|_: Infallible| unreachable!()))
|
||||||
|
}
|
||||||
|
pub fn error_body(s: String) -> ErrorBody {
|
||||||
|
ErrorBody::from(bytes::Bytes::from(s.into_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn parse_json_body<T, B, E>(req: Request<B>) -> Result<T, E>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
B: Body,
|
||||||
|
E: From<<B as Body>::Error> + From<Error>,
|
||||||
|
{
|
||||||
|
let body = req.into_body().collect().await?.to_bytes();
|
||||||
let resp: T = serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?;
|
let resp: T = serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?;
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn json_ok_response<T: Serialize>(res: &T) -> Result<Response<Body>, Error> {
|
pub fn json_ok_response<E, T: Serialize>(res: &T) -> Result<Response<BoxBody<E>>, E>
|
||||||
let resp_json = serde_json::to_string_pretty(res).map_err(garage_util::error::Error::from)?;
|
where
|
||||||
|
E: From<Error>,
|
||||||
|
{
|
||||||
|
let resp_json = serde_json::to_string_pretty(res)
|
||||||
|
.map_err(GarageError::from)
|
||||||
|
.map_err(Error::from)?;
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(hyper::StatusCode::OK)
|
.status(hyper::StatusCode::OK)
|
||||||
.header(http::header::CONTENT_TYPE, "application/json")
|
.header(http::header::CONTENT_TYPE, "application/json")
|
||||||
.body(Body::from(resp_json))?)
|
.body(string_body(resp_json))
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn body_stream<B, E>(body: B) -> impl Stream<Item = Result<Bytes, E>>
|
||||||
|
where
|
||||||
|
B: Body<Data = Bytes>,
|
||||||
|
<B as Body>::Error: Into<E>,
|
||||||
|
E: From<Error>,
|
||||||
|
{
|
||||||
|
let stream = http_body_util::BodyStream::new(body);
|
||||||
|
let stream = TryStreamExt::map_err(stream, Into::into);
|
||||||
|
stream.map(|x| {
|
||||||
|
x.and_then(|f| {
|
||||||
|
f.into_data()
|
||||||
|
.map_err(|_| E::from(Error::bad_request("non-data frame")))
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_default<T: Default + PartialEq>(v: &T) -> bool {
|
pub fn is_default<T: Default + PartialEq>(v: &T) -> bool {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use futures::future::Future;
|
use futures::future::Future;
|
||||||
use hyper::{Body, Method, Request, Response};
|
use hyper::{body::Incoming as IncomingBody, Method, Request, Response};
|
||||||
|
|
||||||
use opentelemetry::{trace::SpanRef, KeyValue};
|
use opentelemetry::{trace::SpanRef, KeyValue};
|
||||||
|
|
||||||
|
@ -25,6 +25,9 @@ use crate::k2v::item::*;
|
||||||
use crate::k2v::router::Endpoint;
|
use crate::k2v::router::Endpoint;
|
||||||
use crate::s3::cors::*;
|
use crate::s3::cors::*;
|
||||||
|
|
||||||
|
pub use crate::signature::streaming::ReqBody;
|
||||||
|
pub type ResBody = BoxBody<Error>;
|
||||||
|
|
||||||
pub struct K2VApiServer {
|
pub struct K2VApiServer {
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
}
|
}
|
||||||
|
@ -55,7 +58,7 @@ impl ApiHandler for K2VApiServer {
|
||||||
type Endpoint = K2VApiEndpoint;
|
type Endpoint = K2VApiEndpoint;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn parse_endpoint(&self, req: &Request<Body>) -> Result<K2VApiEndpoint, Error> {
|
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<K2VApiEndpoint, Error> {
|
||||||
let (endpoint, bucket_name) = Endpoint::from_request(req)?;
|
let (endpoint, bucket_name) = Endpoint::from_request(req)?;
|
||||||
|
|
||||||
Ok(K2VApiEndpoint {
|
Ok(K2VApiEndpoint {
|
||||||
|
@ -66,9 +69,9 @@ impl ApiHandler for K2VApiServer {
|
||||||
|
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
endpoint: K2VApiEndpoint,
|
endpoint: K2VApiEndpoint,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let K2VApiEndpoint {
|
let K2VApiEndpoint {
|
||||||
bucket_name,
|
bucket_name,
|
||||||
endpoint,
|
endpoint,
|
||||||
|
@ -77,9 +80,10 @@ impl ApiHandler for K2VApiServer {
|
||||||
|
|
||||||
// The OPTIONS method is procesed early, before we even check for an API key
|
// The OPTIONS method is procesed early, before we even check for an API key
|
||||||
if let Endpoint::Options = endpoint {
|
if let Endpoint::Options = endpoint {
|
||||||
return Ok(handle_options_s3api(garage, &req, Some(bucket_name))
|
let options_res = handle_options_api(garage, &req, Some(bucket_name))
|
||||||
.await
|
.await
|
||||||
.ok_or_bad_request("Error handling OPTIONS")?);
|
.ok_or_bad_request("Error handling OPTIONS")?;
|
||||||
|
return Ok(options_res.map(|_empty_body: EmptyBody| empty_body()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (api_key, mut content_sha256) = check_payload_signature(&garage, "k2v", &req).await?;
|
let (api_key, mut content_sha256) = check_payload_signature(&garage, "k2v", &req).await?;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use hyper::{Request, Response, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
@ -13,15 +13,16 @@ use garage_model::k2v::causality::*;
|
||||||
use garage_model::k2v::item_table::*;
|
use garage_model::k2v::item_table::*;
|
||||||
|
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
|
use crate::k2v::api_server::{ReqBody, ResBody};
|
||||||
use crate::k2v::error::*;
|
use crate::k2v::error::*;
|
||||||
use crate::k2v::range::read_range;
|
use crate::k2v::range::read_range;
|
||||||
|
|
||||||
pub async fn handle_insert_batch(
|
pub async fn handle_insert_batch(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let items = parse_json_body::<Vec<InsertBatchItem>>(req).await?;
|
let items = parse_json_body::<Vec<InsertBatchItem>, _, Error>(req).await?;
|
||||||
|
|
||||||
let mut items2 = vec![];
|
let mut items2 = vec![];
|
||||||
for it in items {
|
for it in items {
|
||||||
|
@ -41,15 +42,15 @@ pub async fn handle_insert_batch(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_read_batch(
|
pub async fn handle_read_batch(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let queries = parse_json_body::<Vec<ReadBatchQuery>>(req).await?;
|
let queries = parse_json_body::<Vec<ReadBatchQuery>, _, Error>(req).await?;
|
||||||
|
|
||||||
let resp_results = futures::future::join_all(
|
let resp_results = futures::future::join_all(
|
||||||
queries
|
queries
|
||||||
|
@ -139,9 +140,9 @@ async fn handle_read_batch_query(
|
||||||
pub async fn handle_delete_batch(
|
pub async fn handle_delete_batch(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let queries = parse_json_body::<Vec<DeleteBatchQuery>>(req).await?;
|
let queries = parse_json_body::<Vec<DeleteBatchQuery>, _, Error>(req).await?;
|
||||||
|
|
||||||
let resp_results = futures::future::join_all(
|
let resp_results = futures::future::join_all(
|
||||||
queries
|
queries
|
||||||
|
@ -253,11 +254,11 @@ pub(crate) async fn handle_poll_range(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
partition_key: &str,
|
partition_key: &str,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
use garage_model::k2v::sub::PollRange;
|
use garage_model::k2v::sub::PollRange;
|
||||||
|
|
||||||
let query = parse_json_body::<PollRangeQuery>(req).await?;
|
let query = parse_json_body::<PollRangeQuery, _, Error>(req).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;
|
||||||
|
|
||||||
|
@ -292,7 +293,7 @@ pub(crate) async fn handle_poll_range(
|
||||||
} else {
|
} else {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NOT_MODIFIED)
|
.status(StatusCode::NOT_MODIFIED)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
use err_derive::Error;
|
use err_derive::Error;
|
||||||
use hyper::header::HeaderValue;
|
use hyper::header::HeaderValue;
|
||||||
use hyper::{Body, HeaderMap, StatusCode};
|
use hyper::{HeaderMap, StatusCode};
|
||||||
|
|
||||||
use garage_model::helper::error::Error as HelperError;
|
|
||||||
|
|
||||||
use crate::common_error::CommonError;
|
use crate::common_error::CommonError;
|
||||||
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
||||||
use crate::generic_server::ApiError;
|
use crate::generic_server::ApiError;
|
||||||
use crate::helpers::CustomApiErrorBody;
|
use crate::helpers::*;
|
||||||
use crate::signature::error::Error as SignatureError;
|
use crate::signature::error::Error as SignatureError;
|
||||||
|
|
||||||
/// Errors of this crate
|
/// Errors of this crate
|
||||||
|
@ -30,10 +28,6 @@ pub enum Error {
|
||||||
#[error(display = "Invalid base64: {}", _0)]
|
#[error(display = "Invalid base64: {}", _0)]
|
||||||
InvalidBase64(#[error(source)] base64::DecodeError),
|
InvalidBase64(#[error(source)] base64::DecodeError),
|
||||||
|
|
||||||
/// The client sent a header with invalid value
|
|
||||||
#[error(display = "Invalid header value: {}", _0)]
|
|
||||||
InvalidHeader(#[error(source)] hyper::header::ToStrError),
|
|
||||||
|
|
||||||
/// The client asked for an invalid return format (invalid Accept header)
|
/// The client asked for an invalid return format (invalid Accept header)
|
||||||
#[error(display = "Not acceptable: {}", _0)]
|
#[error(display = "Not acceptable: {}", _0)]
|
||||||
NotAcceptable(String),
|
NotAcceptable(String),
|
||||||
|
@ -54,18 +48,6 @@ where
|
||||||
|
|
||||||
impl CommonErrorDerivative for Error {}
|
impl CommonErrorDerivative for Error {}
|
||||||
|
|
||||||
impl From<HelperError> for Error {
|
|
||||||
fn from(err: HelperError) -> Self {
|
|
||||||
match err {
|
|
||||||
HelperError::Internal(i) => Self::Common(CommonError::InternalError(i)),
|
|
||||||
HelperError::BadRequest(b) => Self::Common(CommonError::BadRequest(b)),
|
|
||||||
HelperError::InvalidBucketName(n) => Self::Common(CommonError::InvalidBucketName(n)),
|
|
||||||
HelperError::NoSuchBucket(n) => Self::Common(CommonError::NoSuchBucket(n)),
|
|
||||||
e => Self::Common(CommonError::BadRequest(format!("{}", e))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SignatureError> for Error {
|
impl From<SignatureError> for Error {
|
||||||
fn from(err: SignatureError) -> Self {
|
fn from(err: SignatureError) -> Self {
|
||||||
match err {
|
match err {
|
||||||
|
@ -74,7 +56,6 @@ impl From<SignatureError> for Error {
|
||||||
Self::AuthorizationHeaderMalformed(c)
|
Self::AuthorizationHeaderMalformed(c)
|
||||||
}
|
}
|
||||||
SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i),
|
SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i),
|
||||||
SignatureError::InvalidHeader(h) => Self::InvalidHeader(h),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +71,6 @@ impl Error {
|
||||||
Error::NotAcceptable(_) => "NotAcceptable",
|
Error::NotAcceptable(_) => "NotAcceptable",
|
||||||
Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed",
|
Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed",
|
||||||
Error::InvalidBase64(_) => "InvalidBase64",
|
Error::InvalidBase64(_) => "InvalidBase64",
|
||||||
Error::InvalidHeader(_) => "InvalidHeaderValue",
|
|
||||||
Error::InvalidUtf8Str(_) => "InvalidUtf8String",
|
Error::InvalidUtf8Str(_) => "InvalidUtf8String",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +85,6 @@ impl ApiError for Error {
|
||||||
Error::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE,
|
Error::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE,
|
||||||
Error::AuthorizationHeaderMalformed(_)
|
Error::AuthorizationHeaderMalformed(_)
|
||||||
| Error::InvalidBase64(_)
|
| Error::InvalidBase64(_)
|
||||||
| Error::InvalidHeader(_)
|
|
||||||
| Error::InvalidUtf8Str(_) => StatusCode::BAD_REQUEST,
|
| Error::InvalidUtf8Str(_) => StatusCode::BAD_REQUEST,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,14 +94,14 @@ impl ApiError for Error {
|
||||||
header_map.append(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
header_map.append(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn http_body(&self, garage_region: &str, path: &str) -> Body {
|
fn http_body(&self, garage_region: &str, path: &str) -> ErrorBody {
|
||||||
let error = CustomApiErrorBody {
|
let error = CustomApiErrorBody {
|
||||||
code: self.code().to_string(),
|
code: self.code().to_string(),
|
||||||
message: format!("{}", self),
|
message: format!("{}", self),
|
||||||
path: path.to_string(),
|
path: path.to_string(),
|
||||||
region: garage_region.to_string(),
|
region: garage_region.to_string(),
|
||||||
};
|
};
|
||||||
Body::from(serde_json::to_string_pretty(&error).unwrap_or_else(|_| {
|
let error_str = serde_json::to_string_pretty(&error).unwrap_or_else(|_| {
|
||||||
r#"
|
r#"
|
||||||
{
|
{
|
||||||
"code": "InternalError",
|
"code": "InternalError",
|
||||||
|
@ -130,6 +109,7 @@ impl ApiError for Error {
|
||||||
}
|
}
|
||||||
"#
|
"#
|
||||||
.into()
|
.into()
|
||||||
}))
|
});
|
||||||
|
error_body(error_str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hyper::{Body, Response};
|
use hyper::Response;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
@ -12,6 +12,7 @@ use garage_model::garage::Garage;
|
||||||
use garage_model::k2v::item_table::{BYTES, CONFLICTS, ENTRIES, VALUES};
|
use garage_model::k2v::item_table::{BYTES, CONFLICTS, ENTRIES, VALUES};
|
||||||
|
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
|
use crate::k2v::api_server::ResBody;
|
||||||
use crate::k2v::error::*;
|
use crate::k2v::error::*;
|
||||||
use crate::k2v::range::read_range;
|
use crate::k2v::range::read_range;
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ pub async fn handle_read_index(
|
||||||
end: Option<String>,
|
end: Option<String>,
|
||||||
limit: Option<u64>,
|
limit: Option<u64>,
|
||||||
reverse: Option<bool>,
|
reverse: Option<bool>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let reverse = reverse.unwrap_or(false);
|
let reverse = reverse.unwrap_or(false);
|
||||||
|
|
||||||
let ring: Arc<Ring> = garage.system.ring.borrow().clone();
|
let ring: Arc<Ring> = garage.system.ring.borrow().clone();
|
||||||
|
@ -68,7 +69,7 @@ pub async fn handle_read_index(
|
||||||
next_start,
|
next_start,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(json_ok_response(&resp)?)
|
json_ok_response::<Error, _>(&resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
use http::header;
|
use http::header;
|
||||||
|
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use hyper::{Request, Response, StatusCode};
|
||||||
|
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ use garage_model::garage::Garage;
|
||||||
use garage_model::k2v::causality::*;
|
use garage_model::k2v::causality::*;
|
||||||
use garage_model::k2v::item_table::*;
|
use garage_model::k2v::item_table::*;
|
||||||
|
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::k2v::api_server::{ReqBody, ResBody};
|
||||||
use crate::k2v::error::*;
|
use crate::k2v::error::*;
|
||||||
|
|
||||||
pub const X_GARAGE_CAUSALITY_TOKEN: &str = "X-Garage-Causality-Token";
|
pub const X_GARAGE_CAUSALITY_TOKEN: &str = "X-Garage-Causality-Token";
|
||||||
|
@ -22,7 +24,7 @@ pub enum ReturnFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReturnFormat {
|
impl ReturnFormat {
|
||||||
pub fn from(req: &Request<Body>) -> Result<Self, Error> {
|
pub fn from(req: &Request<ReqBody>) -> Result<Self, Error> {
|
||||||
let accept = match req.headers().get(header::ACCEPT) {
|
let accept = match req.headers().get(header::ACCEPT) {
|
||||||
Some(a) => a.to_str()?,
|
Some(a) => a.to_str()?,
|
||||||
None => return Ok(Self::Json),
|
None => return Ok(Self::Json),
|
||||||
|
@ -40,7 +42,7 @@ impl ReturnFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_response(&self, item: &K2VItem) -> Result<Response<Body>, Error> {
|
pub fn make_response(&self, item: &K2VItem) -> Result<Response<ResBody>, Error> {
|
||||||
let vals = item.values();
|
let vals = item.values();
|
||||||
|
|
||||||
if vals.is_empty() {
|
if vals.is_empty() {
|
||||||
|
@ -52,7 +54,7 @@ impl ReturnFormat {
|
||||||
Self::Binary if vals.len() > 1 => Ok(Response::builder()
|
Self::Binary if vals.len() > 1 => Ok(Response::builder()
|
||||||
.header(X_GARAGE_CAUSALITY_TOKEN, ct)
|
.header(X_GARAGE_CAUSALITY_TOKEN, ct)
|
||||||
.status(StatusCode::CONFLICT)
|
.status(StatusCode::CONFLICT)
|
||||||
.body(Body::empty())?),
|
.body(empty_body())?),
|
||||||
Self::Binary => {
|
Self::Binary => {
|
||||||
assert!(vals.len() == 1);
|
assert!(vals.len() == 1);
|
||||||
Self::make_binary_response(ct, vals[0])
|
Self::make_binary_response(ct, vals[0])
|
||||||
|
@ -62,22 +64,22 @@ impl ReturnFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_binary_response(ct: String, v: &DvvsValue) -> Result<Response<Body>, Error> {
|
fn make_binary_response(ct: String, v: &DvvsValue) -> Result<Response<ResBody>, Error> {
|
||||||
match v {
|
match v {
|
||||||
DvvsValue::Deleted => Ok(Response::builder()
|
DvvsValue::Deleted => Ok(Response::builder()
|
||||||
.header(X_GARAGE_CAUSALITY_TOKEN, ct)
|
.header(X_GARAGE_CAUSALITY_TOKEN, ct)
|
||||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?),
|
.body(empty_body())?),
|
||||||
DvvsValue::Value(v) => Ok(Response::builder()
|
DvvsValue::Value(v) => Ok(Response::builder()
|
||||||
.header(X_GARAGE_CAUSALITY_TOKEN, ct)
|
.header(X_GARAGE_CAUSALITY_TOKEN, ct)
|
||||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::from(v.to_vec()))?),
|
.body(bytes_body(v.to_vec().into()))?),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_json_response(ct: String, v: &[&DvvsValue]) -> Result<Response<Body>, Error> {
|
fn make_json_response(ct: String, v: &[&DvvsValue]) -> Result<Response<ResBody>, Error> {
|
||||||
let items = v
|
let items = v
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| match v {
|
.map(|v| match v {
|
||||||
|
@ -91,7 +93,7 @@ impl ReturnFormat {
|
||||||
.header(X_GARAGE_CAUSALITY_TOKEN, ct)
|
.header(X_GARAGE_CAUSALITY_TOKEN, ct)
|
||||||
.header(header::CONTENT_TYPE, "application/json")
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::from(json_body))?)
|
.body(string_body(json_body))?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,11 +101,11 @@ impl ReturnFormat {
|
||||||
#[allow(clippy::ptr_arg)]
|
#[allow(clippy::ptr_arg)]
|
||||||
pub async fn handle_read_item(
|
pub async fn handle_read_item(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: &Request<Body>,
|
req: &Request<ReqBody>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
partition_key: &str,
|
partition_key: &str,
|
||||||
sort_key: &String,
|
sort_key: &String,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let format = ReturnFormat::from(req)?;
|
let format = ReturnFormat::from(req)?;
|
||||||
|
|
||||||
let item = garage
|
let item = garage
|
||||||
|
@ -124,11 +126,11 @@ pub async fn handle_read_item(
|
||||||
|
|
||||||
pub async fn handle_insert_item(
|
pub async fn handle_insert_item(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
partition_key: &str,
|
partition_key: &str,
|
||||||
sort_key: &str,
|
sort_key: &str,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let causal_context = req
|
let causal_context = req
|
||||||
.headers()
|
.headers()
|
||||||
.get(X_GARAGE_CAUSALITY_TOKEN)
|
.get(X_GARAGE_CAUSALITY_TOKEN)
|
||||||
|
@ -137,7 +139,10 @@ pub async fn handle_insert_item(
|
||||||
.map(CausalContext::parse_helper)
|
.map(CausalContext::parse_helper)
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
let body = http_body_util::BodyExt::collect(req.into_body())
|
||||||
|
.await?
|
||||||
|
.to_bytes();
|
||||||
|
|
||||||
let value = DvvsValue::Value(body.to_vec());
|
let value = DvvsValue::Value(body.to_vec());
|
||||||
|
|
||||||
garage
|
garage
|
||||||
|
@ -154,16 +159,16 @@ pub async fn handle_insert_item(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_delete_item(
|
pub async fn handle_delete_item(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
partition_key: &str,
|
partition_key: &str,
|
||||||
sort_key: &str,
|
sort_key: &str,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let causal_context = req
|
let causal_context = req
|
||||||
.headers()
|
.headers()
|
||||||
.get(X_GARAGE_CAUSALITY_TOKEN)
|
.get(X_GARAGE_CAUSALITY_TOKEN)
|
||||||
|
@ -188,20 +193,20 @@ pub async fn handle_delete_item(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle ReadItem request
|
/// Handle ReadItem request
|
||||||
#[allow(clippy::ptr_arg)]
|
#[allow(clippy::ptr_arg)]
|
||||||
pub async fn handle_poll_item(
|
pub async fn handle_poll_item(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: &Request<Body>,
|
req: &Request<ReqBody>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
partition_key: String,
|
partition_key: String,
|
||||||
sort_key: String,
|
sort_key: String,
|
||||||
causality_token: String,
|
causality_token: String,
|
||||||
timeout_secs: Option<u64>,
|
timeout_secs: Option<u64>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let format = ReturnFormat::from(req)?;
|
let format = ReturnFormat::from(req)?;
|
||||||
|
|
||||||
let causal_context =
|
let causal_context =
|
||||||
|
@ -226,6 +231,6 @@ pub async fn handle_poll_item(
|
||||||
} else {
|
} else {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NOT_MODIFIED)
|
.status(StatusCode::NOT_MODIFIED)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use async_trait::async_trait;
|
||||||
|
|
||||||
use futures::future::Future;
|
use futures::future::Future;
|
||||||
use hyper::header;
|
use hyper::header;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{body::Incoming as IncomingBody, Request, Response};
|
||||||
|
|
||||||
use opentelemetry::{trace::SpanRef, KeyValue};
|
use opentelemetry::{trace::SpanRef, KeyValue};
|
||||||
|
|
||||||
|
@ -34,6 +34,9 @@ use crate::s3::put::*;
|
||||||
use crate::s3::router::Endpoint;
|
use crate::s3::router::Endpoint;
|
||||||
use crate::s3::website::*;
|
use crate::s3::website::*;
|
||||||
|
|
||||||
|
pub use crate::signature::streaming::ReqBody;
|
||||||
|
pub type ResBody = BoxBody<Error>;
|
||||||
|
|
||||||
pub struct S3ApiServer {
|
pub struct S3ApiServer {
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
}
|
}
|
||||||
|
@ -57,10 +60,10 @@ impl S3ApiServer {
|
||||||
|
|
||||||
async fn handle_request_without_bucket(
|
async fn handle_request_without_bucket(
|
||||||
&self,
|
&self,
|
||||||
_req: Request<Body>,
|
_req: Request<ReqBody>,
|
||||||
api_key: Key,
|
api_key: Key,
|
||||||
endpoint: Endpoint,
|
endpoint: Endpoint,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
match endpoint {
|
match endpoint {
|
||||||
Endpoint::ListBuckets => handle_list_buckets(&self.garage, &api_key).await,
|
Endpoint::ListBuckets => handle_list_buckets(&self.garage, &api_key).await,
|
||||||
endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())),
|
endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())),
|
||||||
|
@ -76,7 +79,7 @@ impl ApiHandler for S3ApiServer {
|
||||||
type Endpoint = S3ApiEndpoint;
|
type Endpoint = S3ApiEndpoint;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn parse_endpoint(&self, req: &Request<Body>) -> Result<S3ApiEndpoint, Error> {
|
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<S3ApiEndpoint, Error> {
|
||||||
let authority = req
|
let authority = req
|
||||||
.headers()
|
.headers()
|
||||||
.get(header::HOST)
|
.get(header::HOST)
|
||||||
|
@ -104,9 +107,9 @@ impl ApiHandler for S3ApiServer {
|
||||||
|
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
endpoint: S3ApiEndpoint,
|
endpoint: S3ApiEndpoint,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let S3ApiEndpoint {
|
let S3ApiEndpoint {
|
||||||
bucket_name,
|
bucket_name,
|
||||||
endpoint,
|
endpoint,
|
||||||
|
@ -118,7 +121,8 @@ impl ApiHandler for S3ApiServer {
|
||||||
return handle_post_object(garage, req, bucket_name.unwrap()).await;
|
return handle_post_object(garage, req, bucket_name.unwrap()).await;
|
||||||
}
|
}
|
||||||
if let Endpoint::Options = endpoint {
|
if let Endpoint::Options = endpoint {
|
||||||
return handle_options_s3api(garage, &req, bucket_name).await;
|
let options_res = handle_options_api(garage, &req, bucket_name).await?;
|
||||||
|
return Ok(options_res.map(|_empty_body: EmptyBody| empty_body()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (api_key, mut content_sha256) = check_payload_signature(&garage, "s3", &req).await?;
|
let (api_key, mut content_sha256) = check_payload_signature(&garage, "s3", &req).await?;
|
||||||
|
@ -235,8 +239,7 @@ impl ApiHandler for S3ApiServer {
|
||||||
}
|
}
|
||||||
Endpoint::CreateBucket {} => unreachable!(),
|
Endpoint::CreateBucket {} => unreachable!(),
|
||||||
Endpoint::HeadBucket {} => {
|
Endpoint::HeadBucket {} => {
|
||||||
let empty_body: Body = Body::from(vec![]);
|
let response = Response::builder().body(empty_body()).unwrap();
|
||||||
let response = Response::builder().body(empty_body).unwrap();
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
Endpoint::DeleteBucket {} => {
|
Endpoint::DeleteBucket {} => {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use http_body_util::BodyExt;
|
||||||
|
use hyper::{Request, Response, StatusCode};
|
||||||
|
|
||||||
use garage_model::bucket_alias_table::*;
|
use garage_model::bucket_alias_table::*;
|
||||||
use garage_model::bucket_table::Bucket;
|
use garage_model::bucket_table::Bucket;
|
||||||
|
@ -14,11 +15,13 @@ use garage_util::data::*;
|
||||||
use garage_util::time::*;
|
use garage_util::time::*;
|
||||||
|
|
||||||
use crate::common_error::CommonError;
|
use crate::common_error::CommonError;
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::{ReqBody, ResBody};
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::xml as s3_xml;
|
use crate::s3::xml as s3_xml;
|
||||||
use crate::signature::verify_signed_content;
|
use crate::signature::verify_signed_content;
|
||||||
|
|
||||||
pub fn handle_get_bucket_location(garage: Arc<Garage>) -> Result<Response<Body>, Error> {
|
pub fn handle_get_bucket_location(garage: Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||||
let loc = s3_xml::LocationConstraint {
|
let loc = s3_xml::LocationConstraint {
|
||||||
xmlns: (),
|
xmlns: (),
|
||||||
region: garage.config.s3_api.s3_region.to_string(),
|
region: garage.config.s3_api.s3_region.to_string(),
|
||||||
|
@ -27,10 +30,10 @@ pub fn handle_get_bucket_location(garage: Arc<Garage>) -> Result<Response<Body>,
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("Content-Type", "application/xml")
|
.header("Content-Type", "application/xml")
|
||||||
.body(Body::from(xml.into_bytes()))?)
|
.body(string_body(xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_get_bucket_versioning() -> Result<Response<Body>, Error> {
|
pub fn handle_get_bucket_versioning() -> Result<Response<ResBody>, Error> {
|
||||||
let versioning = s3_xml::VersioningConfiguration {
|
let versioning = s3_xml::VersioningConfiguration {
|
||||||
xmlns: (),
|
xmlns: (),
|
||||||
status: None,
|
status: None,
|
||||||
|
@ -40,10 +43,13 @@ pub fn handle_get_bucket_versioning() -> Result<Response<Body>, Error> {
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("Content-Type", "application/xml")
|
.header("Content-Type", "application/xml")
|
||||||
.body(Body::from(xml.into_bytes()))?)
|
.body(string_body(xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_list_buckets(garage: &Garage, api_key: &Key) -> Result<Response<Body>, Error> {
|
pub async fn handle_list_buckets(
|
||||||
|
garage: &Garage,
|
||||||
|
api_key: &Key,
|
||||||
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let key_p = api_key.params().ok_or_internal_error(
|
let key_p = api_key.params().ok_or_internal_error(
|
||||||
"Key should not be in deleted state at this point (in handle_list_buckets)",
|
"Key should not be in deleted state at this point (in handle_list_buckets)",
|
||||||
)?;
|
)?;
|
||||||
|
@ -109,17 +115,17 @@ pub async fn handle_list_buckets(garage: &Garage, api_key: &Key) -> Result<Respo
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("Content-Type", "application/xml")
|
.header("Content-Type", "application/xml")
|
||||||
.body(Body::from(xml))?)
|
.body(string_body(xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_create_bucket(
|
pub async fn handle_create_bucket(
|
||||||
garage: &Garage,
|
garage: &Garage,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
content_sha256: Option<Hash>,
|
content_sha256: Option<Hash>,
|
||||||
api_key: Key,
|
api_key: Key,
|
||||||
bucket_name: String,
|
bucket_name: String,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
let body = BodyExt::collect(req.into_body()).await?.to_bytes();
|
||||||
|
|
||||||
if let Some(content_sha256) = content_sha256 {
|
if let Some(content_sha256) = content_sha256 {
|
||||||
verify_signed_content(content_sha256, &body[..])?;
|
verify_signed_content(content_sha256, &body[..])?;
|
||||||
|
@ -194,7 +200,7 @@ pub async fn handle_create_bucket(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("Location", format!("/{}", bucket_name))
|
.header("Location", format!("/{}", bucket_name))
|
||||||
.body(Body::empty())
|
.body(empty_body())
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +209,7 @@ pub async fn handle_delete_bucket(
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
bucket_name: String,
|
bucket_name: String,
|
||||||
api_key: Key,
|
api_key: Key,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let key_params = api_key
|
let key_params = api_key
|
||||||
.params()
|
.params()
|
||||||
.ok_or_internal_error("Key should not be deleted at this point")?;
|
.ok_or_internal_error("Key should not be deleted at this point")?;
|
||||||
|
@ -282,7 +288,7 @@ pub async fn handle_delete_bucket(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_create_bucket_xml(xml_bytes: &[u8]) -> Option<Option<String>> {
|
fn parse_create_bucket_xml(xml_bytes: &[u8]) -> Option<Option<String>> {
|
||||||
|
|
|
@ -6,7 +6,7 @@ use futures::{stream, stream::Stream, StreamExt};
|
||||||
use md5::{Digest as Md5Digest, Md5};
|
use md5::{Digest as Md5Digest, Md5};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Request, Response};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use garage_rpc::netapp::bytes_buf::BytesBuf;
|
use garage_rpc::netapp::bytes_buf::BytesBuf;
|
||||||
|
@ -22,7 +22,8 @@ use garage_model::s3::mpu_table::*;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
use garage_model::s3::version_table::*;
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
use crate::helpers::parse_bucket_key;
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::{ReqBody, ResBody};
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::multipart;
|
use crate::s3::multipart;
|
||||||
use crate::s3::put::get_headers;
|
use crate::s3::put::get_headers;
|
||||||
|
@ -31,10 +32,10 @@ use crate::s3::xml::{self as s3_xml, xmlns_tag};
|
||||||
pub async fn handle_copy(
|
pub async fn handle_copy(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
api_key: &Key,
|
api_key: &Key,
|
||||||
req: &Request<Body>,
|
req: &Request<ReqBody>,
|
||||||
dest_bucket_id: Uuid,
|
dest_bucket_id: Uuid,
|
||||||
dest_key: &str,
|
dest_key: &str,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let copy_precondition = CopyPreconditionHeaders::parse(req)?;
|
let copy_precondition = CopyPreconditionHeaders::parse(req)?;
|
||||||
|
|
||||||
let source_object = get_copy_source(&garage, api_key, req).await?;
|
let source_object = get_copy_source(&garage, api_key, req).await?;
|
||||||
|
@ -176,18 +177,18 @@ pub async fn handle_copy(
|
||||||
"x-amz-copy-source-version-id",
|
"x-amz-copy-source-version-id",
|
||||||
hex::encode(source_version.uuid),
|
hex::encode(source_version.uuid),
|
||||||
)
|
)
|
||||||
.body(Body::from(xml))?)
|
.body(string_body(xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_upload_part_copy(
|
pub async fn handle_upload_part_copy(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
api_key: &Key,
|
api_key: &Key,
|
||||||
req: &Request<Body>,
|
req: &Request<ReqBody>,
|
||||||
dest_bucket_id: Uuid,
|
dest_bucket_id: Uuid,
|
||||||
dest_key: &str,
|
dest_key: &str,
|
||||||
part_number: u64,
|
part_number: u64,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let copy_precondition = CopyPreconditionHeaders::parse(req)?;
|
let copy_precondition = CopyPreconditionHeaders::parse(req)?;
|
||||||
|
|
||||||
let dest_upload_id = multipart::decode_upload_id(upload_id)?;
|
let dest_upload_id = multipart::decode_upload_id(upload_id)?;
|
||||||
|
@ -432,13 +433,13 @@ pub async fn handle_upload_part_copy(
|
||||||
"x-amz-copy-source-version-id",
|
"x-amz-copy-source-version-id",
|
||||||
hex::encode(source_object_version.uuid),
|
hex::encode(source_object_version.uuid),
|
||||||
)
|
)
|
||||||
.body(Body::from(resp_xml))?)
|
.body(string_body(resp_xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_copy_source(
|
async fn get_copy_source(
|
||||||
garage: &Garage,
|
garage: &Garage,
|
||||||
api_key: &Key,
|
api_key: &Key,
|
||||||
req: &Request<Body>,
|
req: &Request<ReqBody>,
|
||||||
) -> Result<Object, Error> {
|
) -> Result<Object, Error> {
|
||||||
let copy_source = req.headers().get("x-amz-copy-source").unwrap().to_str()?;
|
let copy_source = req.headers().get("x-amz-copy-source").unwrap().to_str()?;
|
||||||
let copy_source = percent_encoding::percent_decode_str(copy_source).decode_utf8()?;
|
let copy_source = percent_encoding::percent_decode_str(copy_source).decode_utf8()?;
|
||||||
|
@ -501,7 +502,7 @@ struct CopyPreconditionHeaders {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CopyPreconditionHeaders {
|
impl CopyPreconditionHeaders {
|
||||||
fn parse(req: &Request<Body>) -> Result<Self, Error> {
|
fn parse(req: &Request<ReqBody>) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
copy_source_if_match: req
|
copy_source_if_match: req
|
||||||
.headers()
|
.headers()
|
||||||
|
|
|
@ -5,10 +5,18 @@ use http::header::{
|
||||||
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN,
|
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||||
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD,
|
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD,
|
||||||
};
|
};
|
||||||
use hyper::{header::HeaderName, Body, Method, Request, Response, StatusCode};
|
use hyper::{
|
||||||
|
body::Body, body::Incoming as IncomingBody, header::HeaderName, Method, Request, Response,
|
||||||
|
StatusCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
use http_body_util::BodyExt;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::common_error::CommonError;
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::{ReqBody, ResBody};
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
|
use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
|
||||||
use crate::signature::verify_signed_content;
|
use crate::signature::verify_signed_content;
|
||||||
|
@ -17,7 +25,7 @@ use garage_model::bucket_table::{Bucket, CorsRule as GarageCorsRule};
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
|
||||||
pub async fn handle_get_cors(bucket: &Bucket) -> Result<Response<Body>, Error> {
|
pub async fn handle_get_cors(bucket: &Bucket) -> Result<Response<ResBody>, Error> {
|
||||||
let param = bucket
|
let param = bucket
|
||||||
.params()
|
.params()
|
||||||
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
||||||
|
@ -34,18 +42,18 @@ pub async fn handle_get_cors(bucket: &Bucket) -> Result<Response<Body>, Error> {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header(http::header::CONTENT_TYPE, "application/xml")
|
.header(http::header::CONTENT_TYPE, "application/xml")
|
||||||
.body(Body::from(xml))?)
|
.body(string_body(xml))?)
|
||||||
} else {
|
} else {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_delete_cors(
|
pub async fn handle_delete_cors(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
mut bucket: Bucket,
|
mut bucket: Bucket,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let param = bucket
|
let param = bucket
|
||||||
.params_mut()
|
.params_mut()
|
||||||
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
||||||
|
@ -55,16 +63,16 @@ pub async fn handle_delete_cors(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_put_cors(
|
pub async fn handle_put_cors(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
mut bucket: Bucket,
|
mut bucket: Bucket,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
content_sha256: Option<Hash>,
|
content_sha256: Option<Hash>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
let body = BodyExt::collect(req.into_body()).await?.to_bytes();
|
||||||
|
|
||||||
if let Some(content_sha256) = content_sha256 {
|
if let Some(content_sha256) = content_sha256 {
|
||||||
verify_signed_content(content_sha256, &body[..])?;
|
verify_signed_content(content_sha256, &body[..])?;
|
||||||
|
@ -84,14 +92,14 @@ pub async fn handle_put_cors(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_options_s3api(
|
pub async fn handle_options_api(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: &Request<Body>,
|
req: &Request<IncomingBody>,
|
||||||
bucket_name: Option<String>,
|
bucket_name: Option<String>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<EmptyBody>, CommonError> {
|
||||||
// FIXME: CORS rules of buckets with local aliases are
|
// FIXME: CORS rules of buckets with local aliases are
|
||||||
// not taken into account.
|
// not taken into account.
|
||||||
|
|
||||||
|
@ -121,7 +129,7 @@ pub async fn handle_options_s3api(
|
||||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||||
.header(ACCESS_CONTROL_ALLOW_METHODS, "*")
|
.header(ACCESS_CONTROL_ALLOW_METHODS, "*")
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::empty())?)
|
.body(EmptyBody::new())?)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If there is no bucket name in the request,
|
// If there is no bucket name in the request,
|
||||||
|
@ -131,14 +139,14 @@ pub async fn handle_options_s3api(
|
||||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||||
.header(ACCESS_CONTROL_ALLOW_METHODS, "GET")
|
.header(ACCESS_CONTROL_ALLOW_METHODS, "GET")
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::empty())?)
|
.body(EmptyBody::new())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_options_for_bucket(
|
pub fn handle_options_for_bucket(
|
||||||
req: &Request<Body>,
|
req: &Request<IncomingBody>,
|
||||||
bucket: &Bucket,
|
bucket: &Bucket,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<EmptyBody>, CommonError> {
|
||||||
let origin = req
|
let origin = req
|
||||||
.headers()
|
.headers()
|
||||||
.get("Origin")
|
.get("Origin")
|
||||||
|
@ -161,18 +169,20 @@ pub fn handle_options_for_bucket(
|
||||||
if let Some(rule) = matching_rule {
|
if let Some(rule) = matching_rule {
|
||||||
let mut resp = Response::builder()
|
let mut resp = Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::empty())?;
|
.body(EmptyBody::new())?;
|
||||||
add_cors_headers(&mut resp, rule).ok_or_internal_error("Invalid CORS configuration")?;
|
add_cors_headers(&mut resp, rule).ok_or_internal_error("Invalid CORS configuration")?;
|
||||||
return Ok(resp);
|
return Ok(resp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(Error::forbidden("This CORS request is not allowed."))
|
Err(CommonError::Forbidden(
|
||||||
|
"This CORS request is not allowed.".into(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_matching_cors_rule<'a>(
|
pub fn find_matching_cors_rule<'a>(
|
||||||
bucket: &'a Bucket,
|
bucket: &'a Bucket,
|
||||||
req: &Request<Body>,
|
req: &Request<impl Body>,
|
||||||
) -> Result<Option<&'a GarageCorsRule>, Error> {
|
) -> Result<Option<&'a GarageCorsRule>, Error> {
|
||||||
if let Some(cors_config) = bucket.params().unwrap().cors_config.get() {
|
if let Some(cors_config) = bucket.params().unwrap().cors_config.get() {
|
||||||
if let Some(origin) = req.headers().get("Origin") {
|
if let Some(origin) = req.headers().get("Origin") {
|
||||||
|
@ -209,7 +219,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_cors_headers(
|
pub fn add_cors_headers(
|
||||||
resp: &mut Response<Body>,
|
resp: &mut Response<impl Body>,
|
||||||
rule: &GarageCorsRule,
|
rule: &GarageCorsRule,
|
||||||
) -> Result<(), http::header::InvalidHeaderValue> {
|
) -> Result<(), http::header::InvalidHeaderValue> {
|
||||||
let h = resp.headers_mut();
|
let h = resp.headers_mut();
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use http_body_util::BodyExt;
|
||||||
|
use hyper::{Request, Response, StatusCode};
|
||||||
|
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
|
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::{ReqBody, ResBody};
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::put::next_timestamp;
|
use crate::s3::put::next_timestamp;
|
||||||
use crate::s3::xml as s3_xml;
|
use crate::s3::xml as s3_xml;
|
||||||
|
@ -59,11 +62,11 @@ pub async fn handle_delete(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
key: &str,
|
key: &str,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
match handle_delete_internal(&garage, bucket_id, key).await {
|
match handle_delete_internal(&garage, bucket_id, key).await {
|
||||||
Ok(_) | Err(Error::NoSuchKey) => Ok(Response::builder()
|
Ok(_) | Err(Error::NoSuchKey) => Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::from(vec![]))
|
.body(empty_body())
|
||||||
.unwrap()),
|
.unwrap()),
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
}
|
}
|
||||||
|
@ -72,10 +75,10 @@ pub async fn handle_delete(
|
||||||
pub async fn handle_delete_objects(
|
pub async fn handle_delete_objects(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
content_sha256: Option<Hash>,
|
content_sha256: Option<Hash>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
let body = BodyExt::collect(req.into_body()).await?.to_bytes();
|
||||||
|
|
||||||
if let Some(content_sha256) = content_sha256 {
|
if let Some(content_sha256) = content_sha256 {
|
||||||
verify_signed_content(content_sha256, &body[..])?;
|
verify_signed_content(content_sha256, &body[..])?;
|
||||||
|
@ -118,7 +121,7 @@ pub async fn handle_delete_objects(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("Content-Type", "application/xml")
|
.header("Content-Type", "application/xml")
|
||||||
.body(Body::from(xml))?)
|
.body(string_body(xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeleteRequest {
|
struct DeleteRequest {
|
||||||
|
|
|
@ -2,13 +2,12 @@ use std::convert::TryInto;
|
||||||
|
|
||||||
use err_derive::Error;
|
use err_derive::Error;
|
||||||
use hyper::header::HeaderValue;
|
use hyper::header::HeaderValue;
|
||||||
use hyper::{Body, HeaderMap, StatusCode};
|
use hyper::{HeaderMap, StatusCode};
|
||||||
|
|
||||||
use garage_model::helper::error::Error as HelperError;
|
|
||||||
|
|
||||||
use crate::common_error::CommonError;
|
use crate::common_error::CommonError;
|
||||||
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
||||||
use crate::generic_server::ApiError;
|
use crate::generic_server::ApiError;
|
||||||
|
use crate::helpers::*;
|
||||||
use crate::s3::xml as s3_xml;
|
use crate::s3::xml as s3_xml;
|
||||||
use crate::signature::error::Error as SignatureError;
|
use crate::signature::error::Error as SignatureError;
|
||||||
|
|
||||||
|
@ -62,10 +61,6 @@ pub enum Error {
|
||||||
#[error(display = "Invalid XML: {}", _0)]
|
#[error(display = "Invalid XML: {}", _0)]
|
||||||
InvalidXml(String),
|
InvalidXml(String),
|
||||||
|
|
||||||
/// The client sent a header with invalid value
|
|
||||||
#[error(display = "Invalid header value: {}", _0)]
|
|
||||||
InvalidHeader(#[error(source)] hyper::header::ToStrError),
|
|
||||||
|
|
||||||
/// The client sent a range header with invalid value
|
/// The client sent a range header with invalid value
|
||||||
#[error(display = "Invalid HTTP range: {:?}", _0)]
|
#[error(display = "Invalid HTTP range: {:?}", _0)]
|
||||||
InvalidRange(#[error(from)] (http_range::HttpRangeParseError, u64)),
|
InvalidRange(#[error(from)] (http_range::HttpRangeParseError, u64)),
|
||||||
|
@ -86,18 +81,6 @@ where
|
||||||
|
|
||||||
impl CommonErrorDerivative for Error {}
|
impl CommonErrorDerivative for Error {}
|
||||||
|
|
||||||
impl From<HelperError> for Error {
|
|
||||||
fn from(err: HelperError) -> Self {
|
|
||||||
match err {
|
|
||||||
HelperError::Internal(i) => Self::Common(CommonError::InternalError(i)),
|
|
||||||
HelperError::BadRequest(b) => Self::Common(CommonError::BadRequest(b)),
|
|
||||||
HelperError::InvalidBucketName(n) => Self::Common(CommonError::InvalidBucketName(n)),
|
|
||||||
HelperError::NoSuchBucket(n) => Self::Common(CommonError::NoSuchBucket(n)),
|
|
||||||
e => Self::bad_request(format!("{}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<roxmltree::Error> for Error {
|
impl From<roxmltree::Error> for Error {
|
||||||
fn from(err: roxmltree::Error) -> Self {
|
fn from(err: roxmltree::Error) -> Self {
|
||||||
Self::InvalidXml(format!("{}", err))
|
Self::InvalidXml(format!("{}", err))
|
||||||
|
@ -118,7 +101,6 @@ impl From<SignatureError> for Error {
|
||||||
Self::AuthorizationHeaderMalformed(c)
|
Self::AuthorizationHeaderMalformed(c)
|
||||||
}
|
}
|
||||||
SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i),
|
SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i),
|
||||||
SignatureError::InvalidHeader(h) => Self::InvalidHeader(h),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,9 +125,7 @@ impl Error {
|
||||||
Error::NotImplemented(_) => "NotImplemented",
|
Error::NotImplemented(_) => "NotImplemented",
|
||||||
Error::InvalidXml(_) => "MalformedXML",
|
Error::InvalidXml(_) => "MalformedXML",
|
||||||
Error::InvalidRange(_) => "InvalidRange",
|
Error::InvalidRange(_) => "InvalidRange",
|
||||||
Error::InvalidUtf8Str(_) | Error::InvalidUtf8String(_) | Error::InvalidHeader(_) => {
|
Error::InvalidUtf8Str(_) | Error::InvalidUtf8String(_) => "InvalidRequest",
|
||||||
"InvalidRequest"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,8 +145,7 @@ impl ApiError for Error {
|
||||||
| Error::EntityTooSmall
|
| Error::EntityTooSmall
|
||||||
| Error::InvalidXml(_)
|
| Error::InvalidXml(_)
|
||||||
| Error::InvalidUtf8Str(_)
|
| Error::InvalidUtf8Str(_)
|
||||||
| Error::InvalidUtf8String(_)
|
| Error::InvalidUtf8String(_) => StatusCode::BAD_REQUEST,
|
||||||
| Error::InvalidHeader(_) => StatusCode::BAD_REQUEST,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,22 +168,23 @@ impl ApiError for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn http_body(&self, garage_region: &str, path: &str) -> Body {
|
fn http_body(&self, garage_region: &str, path: &str) -> ErrorBody {
|
||||||
let error = s3_xml::Error {
|
let error = s3_xml::Error {
|
||||||
code: s3_xml::Value(self.aws_code().to_string()),
|
code: s3_xml::Value(self.aws_code().to_string()),
|
||||||
message: s3_xml::Value(format!("{}", self)),
|
message: s3_xml::Value(format!("{}", self)),
|
||||||
resource: Some(s3_xml::Value(path.to_string())),
|
resource: Some(s3_xml::Value(path.to_string())),
|
||||||
region: Some(s3_xml::Value(garage_region.to_string())),
|
region: Some(s3_xml::Value(garage_region.to_string())),
|
||||||
};
|
};
|
||||||
Body::from(s3_xml::to_xml_with_header(&error).unwrap_or_else(|_| {
|
let error_str = s3_xml::to_xml_with_header(&error).unwrap_or_else(|_| {
|
||||||
r#"
|
r#"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Error>
|
<Error>
|
||||||
<Code>InternalError</Code>
|
<Code>InternalError</Code>
|
||||||
<Message>XML encoding of error failed</Message>
|
<Message>XML encoding of error failed</Message>
|
||||||
</Error>
|
</Error>
|
||||||
"#
|
"#
|
||||||
.into()
|
.into()
|
||||||
}))
|
});
|
||||||
|
error_body(error_str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,11 @@ use http::header::{
|
||||||
ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE,
|
ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE,
|
||||||
IF_NONE_MATCH, LAST_MODIFIED, RANGE,
|
IF_NONE_MATCH, LAST_MODIFIED, RANGE,
|
||||||
};
|
};
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use hyper::{body::Body, Request, Response, StatusCode};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use garage_rpc::rpc_helper::{netapp::stream::ByteStream, OrderTag};
|
use garage_block::manager::BlockStream;
|
||||||
|
use garage_rpc::rpc_helper::OrderTag;
|
||||||
use garage_table::EmptyKey;
|
use garage_table::EmptyKey;
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
use garage_util::error::OkOrMessage;
|
use garage_util::error::OkOrMessage;
|
||||||
|
@ -20,6 +21,8 @@ use garage_model::garage::Garage;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
use garage_model::s3::version_table::*;
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::ResBody;
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
|
|
||||||
const X_AMZ_MP_PARTS_COUNT: &str = "x-amz-mp-parts-count";
|
const X_AMZ_MP_PARTS_COUNT: &str = "x-amz-mp-parts-count";
|
||||||
|
@ -52,8 +55,8 @@ fn object_headers(
|
||||||
fn try_answer_cached(
|
fn try_answer_cached(
|
||||||
version: &ObjectVersion,
|
version: &ObjectVersion,
|
||||||
version_meta: &ObjectVersionMeta,
|
version_meta: &ObjectVersionMeta,
|
||||||
req: &Request<Body>,
|
req: &Request<impl Body>,
|
||||||
) -> Option<Response<Body>> {
|
) -> Option<Response<ResBody>> {
|
||||||
// <trinity> It is possible, and is even usually the case, [that both If-None-Match and
|
// <trinity> It is possible, and is even usually the case, [that both If-None-Match and
|
||||||
// If-Modified-Since] are present in a request. In this situation If-None-Match takes
|
// 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
|
// precedence and If-Modified-Since is ignored (as per 6.Precedence from rfc7232). The rational
|
||||||
|
@ -80,7 +83,7 @@ fn try_answer_cached(
|
||||||
Some(
|
Some(
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::NOT_MODIFIED)
|
.status(StatusCode::NOT_MODIFIED)
|
||||||
.body(Body::empty())
|
.body(empty_body())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -91,11 +94,11 @@ fn try_answer_cached(
|
||||||
/// Handle HEAD request
|
/// Handle HEAD request
|
||||||
pub async fn handle_head(
|
pub async fn handle_head(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: &Request<Body>,
|
req: &Request<impl Body>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
key: &str,
|
key: &str,
|
||||||
part_number: Option<u64>,
|
part_number: Option<u64>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let object = garage
|
let object = garage
|
||||||
.object_table
|
.object_table
|
||||||
.get(&bucket_id, &key.to_string())
|
.get(&bucket_id, &key.to_string())
|
||||||
|
@ -138,7 +141,7 @@ pub async fn handle_head(
|
||||||
)
|
)
|
||||||
.header(X_AMZ_MP_PARTS_COUNT, "1")
|
.header(X_AMZ_MP_PARTS_COUNT, "1")
|
||||||
.status(StatusCode::PARTIAL_CONTENT)
|
.status(StatusCode::PARTIAL_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
ObjectVersionData::FirstBlock(_, _) => {
|
ObjectVersionData::FirstBlock(_, _) => {
|
||||||
let version = garage
|
let version = garage
|
||||||
|
@ -163,7 +166,7 @@ pub async fn handle_head(
|
||||||
)
|
)
|
||||||
.header(X_AMZ_MP_PARTS_COUNT, format!("{}", version.n_parts()?))
|
.header(X_AMZ_MP_PARTS_COUNT, format!("{}", version.n_parts()?))
|
||||||
.status(StatusCode::PARTIAL_CONTENT)
|
.status(StatusCode::PARTIAL_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
@ -171,18 +174,18 @@ pub async fn handle_head(
|
||||||
Ok(object_headers(object_version, version_meta)
|
Ok(object_headers(object_version, version_meta)
|
||||||
.header(CONTENT_LENGTH, format!("{}", version_meta.size))
|
.header(CONTENT_LENGTH, format!("{}", version_meta.size))
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle GET request
|
/// Handle GET request
|
||||||
pub async fn handle_get(
|
pub async fn handle_get(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: &Request<Body>,
|
req: &Request<impl Body>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
key: &str,
|
key: &str,
|
||||||
part_number: Option<u64>,
|
part_number: Option<u64>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let object = garage
|
let object = garage
|
||||||
.object_table
|
.object_table
|
||||||
.get(&bucket_id, &key.to_string())
|
.get(&bucket_id, &key.to_string())
|
||||||
|
@ -240,11 +243,10 @@ pub async fn handle_get(
|
||||||
match &last_v_data {
|
match &last_v_data {
|
||||||
ObjectVersionData::DeleteMarker => unreachable!(),
|
ObjectVersionData::DeleteMarker => unreachable!(),
|
||||||
ObjectVersionData::Inline(_, bytes) => {
|
ObjectVersionData::Inline(_, bytes) => {
|
||||||
let body: Body = Body::from(bytes.to_vec());
|
Ok(resp_builder.body(bytes_body(bytes.to_vec().into()))?)
|
||||||
Ok(resp_builder.body(body)?)
|
|
||||||
}
|
}
|
||||||
ObjectVersionData::FirstBlock(_, first_block_hash) => {
|
ObjectVersionData::FirstBlock(_, first_block_hash) => {
|
||||||
let (tx, rx) = mpsc::channel(2);
|
let (tx, rx) = mpsc::channel::<BlockStream>(2);
|
||||||
|
|
||||||
let order_stream = OrderTag::stream();
|
let order_stream = OrderTag::stream();
|
||||||
let first_block_hash = *first_block_hash;
|
let first_block_hash = *first_block_hash;
|
||||||
|
@ -282,20 +284,12 @@ pub async fn handle_get(
|
||||||
{
|
{
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let err = std::io::Error::new(
|
let _ = tx.send(error_stream_item(e)).await;
|
||||||
std::io::ErrorKind::Other,
|
|
||||||
format!("Error while getting object data: {}", e),
|
|
||||||
);
|
|
||||||
let _ = tx
|
|
||||||
.send(Box::pin(stream::once(future::ready(Err(err)))))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let body_stream = tokio_stream::wrappers::ReceiverStream::new(rx).flatten();
|
let body = response_body_from_block_stream(rx);
|
||||||
|
|
||||||
let body = hyper::body::Body::wrap_stream(body_stream);
|
|
||||||
Ok(resp_builder.body(body)?)
|
Ok(resp_builder.body(body)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -308,7 +302,7 @@ async fn handle_get_range(
|
||||||
version_meta: &ObjectVersionMeta,
|
version_meta: &ObjectVersionMeta,
|
||||||
begin: u64,
|
begin: u64,
|
||||||
end: u64,
|
end: u64,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let resp_builder = object_headers(version, version_meta)
|
let resp_builder = object_headers(version, version_meta)
|
||||||
.header(CONTENT_LENGTH, format!("{}", end - begin))
|
.header(CONTENT_LENGTH, format!("{}", end - begin))
|
||||||
.header(
|
.header(
|
||||||
|
@ -321,7 +315,7 @@ async fn handle_get_range(
|
||||||
ObjectVersionData::DeleteMarker => unreachable!(),
|
ObjectVersionData::DeleteMarker => unreachable!(),
|
||||||
ObjectVersionData::Inline(_meta, bytes) => {
|
ObjectVersionData::Inline(_meta, bytes) => {
|
||||||
if end as usize <= bytes.len() {
|
if end as usize <= bytes.len() {
|
||||||
let body: Body = Body::from(bytes[begin as usize..end as usize].to_vec());
|
let body = bytes_body(bytes[begin as usize..end as usize].to_vec().into());
|
||||||
Ok(resp_builder.body(body)?)
|
Ok(resp_builder.body(body)?)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::internal_error(
|
Err(Error::internal_error(
|
||||||
|
@ -348,7 +342,7 @@ async fn handle_get_part(
|
||||||
version_data: &ObjectVersionData,
|
version_data: &ObjectVersionData,
|
||||||
version_meta: &ObjectVersionMeta,
|
version_meta: &ObjectVersionMeta,
|
||||||
part_number: u64,
|
part_number: u64,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let resp_builder =
|
let resp_builder =
|
||||||
object_headers(object_version, version_meta).status(StatusCode::PARTIAL_CONTENT);
|
object_headers(object_version, version_meta).status(StatusCode::PARTIAL_CONTENT);
|
||||||
|
|
||||||
|
@ -364,7 +358,7 @@ async fn handle_get_part(
|
||||||
format!("bytes {}-{}/{}", 0, bytes.len() - 1, bytes.len()),
|
format!("bytes {}-{}/{}", 0, bytes.len() - 1, bytes.len()),
|
||||||
)
|
)
|
||||||
.header(X_AMZ_MP_PARTS_COUNT, "1")
|
.header(X_AMZ_MP_PARTS_COUNT, "1")
|
||||||
.body(Body::from(bytes.to_vec()))?)
|
.body(bytes_body(bytes.to_vec().into()))?)
|
||||||
}
|
}
|
||||||
ObjectVersionData::FirstBlock(_, _) => {
|
ObjectVersionData::FirstBlock(_, _) => {
|
||||||
let version = garage
|
let version = garage
|
||||||
|
@ -392,7 +386,7 @@ async fn handle_get_part(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_range_header(
|
fn parse_range_header(
|
||||||
req: &Request<Body>,
|
req: &Request<impl Body>,
|
||||||
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) {
|
||||||
|
@ -434,7 +428,7 @@ fn body_from_blocks_range(
|
||||||
all_blocks: &[(VersionBlockKey, VersionBlock)],
|
all_blocks: &[(VersionBlockKey, VersionBlock)],
|
||||||
begin: u64,
|
begin: u64,
|
||||||
end: u64,
|
end: u64,
|
||||||
) -> Body {
|
) -> ResBody {
|
||||||
// We will store here the list of blocks that have an intersection with the requested
|
// We will store here the list of blocks that have an intersection with the requested
|
||||||
// range, as well as their "true offset", which is their actual offset in the complete
|
// range, as well as their "true offset", which is their actual offset in the complete
|
||||||
// file (whereas block.offset designates the offset of the block WITHIN THE PART
|
// file (whereas block.offset designates the offset of the block WITHIN THE PART
|
||||||
|
@ -456,17 +450,17 @@ fn body_from_blocks_range(
|
||||||
}
|
}
|
||||||
|
|
||||||
let order_stream = OrderTag::stream();
|
let order_stream = OrderTag::stream();
|
||||||
let body_stream = futures::stream::iter(blocks)
|
let (tx, rx) = mpsc::channel::<BlockStream>(2);
|
||||||
.enumerate()
|
|
||||||
.map(move |(i, (block, block_offset))| {
|
tokio::spawn(async move {
|
||||||
|
match async {
|
||||||
let garage = garage.clone();
|
let garage = garage.clone();
|
||||||
async move {
|
for (i, (block, block_offset)) in blocks.iter().enumerate() {
|
||||||
garage
|
let block_stream = garage
|
||||||
.block_manager
|
.block_manager
|
||||||
.rpc_get_block_streaming(&block.hash, Some(order_stream.order(i as u64)))
|
.rpc_get_block_streaming(&block.hash, Some(order_stream.order(i as u64)))
|
||||||
.await
|
.await?
|
||||||
.unwrap_or_else(|e| error_stream(i, e))
|
.scan(*block_offset, move |chunk_offset, chunk| {
|
||||||
.scan(block_offset, move |chunk_offset, chunk| {
|
|
||||||
let r = match chunk {
|
let r = match chunk {
|
||||||
Ok(chunk_bytes) => {
|
Ok(chunk_bytes) => {
|
||||||
let chunk_len = chunk_bytes.len() as u64;
|
let chunk_len = chunk_bytes.len() as u64;
|
||||||
|
@ -502,20 +496,42 @@ fn body_from_blocks_range(
|
||||||
};
|
};
|
||||||
futures::future::ready(r)
|
futures::future::ready(r)
|
||||||
})
|
})
|
||||||
.filter_map(futures::future::ready)
|
.filter_map(futures::future::ready);
|
||||||
|
|
||||||
|
let block_stream: BlockStream = Box::pin(block_stream);
|
||||||
|
tx.send(Box::pin(block_stream))
|
||||||
|
.await
|
||||||
|
.ok_or_message("channel closed")?;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.buffered(2)
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
hyper::body::Body::wrap_stream(body_stream)
|
Ok::<(), Error>(())
|
||||||
|
}
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(e) => {
|
||||||
|
let _ = tx.send(error_stream_item(e)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
response_body_from_block_stream(rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error_stream(i: usize, e: garage_util::error::Error) -> ByteStream {
|
fn response_body_from_block_stream(rx: mpsc::Receiver<BlockStream>) -> ResBody {
|
||||||
Box::pin(futures::stream::once(async move {
|
let body_stream = tokio_stream::wrappers::ReceiverStream::new(rx)
|
||||||
Err(std::io::Error::new(
|
.flatten()
|
||||||
std::io::ErrorKind::Other,
|
.map(|x| {
|
||||||
format!("Could not get block {}: {}", i, e),
|
x.map(hyper::body::Frame::data)
|
||||||
))
|
.map_err(|e| Error::from(garage_util::error::Error::from(e)))
|
||||||
}))
|
});
|
||||||
|
ResBody::new(http_body_util::StreamBody::new(body_stream))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_stream_item<E: std::fmt::Display>(e: E) -> BlockStream {
|
||||||
|
let err = std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
format!("Error while getting object data: {}", e),
|
||||||
|
);
|
||||||
|
Box::pin(stream::once(future::ready(Err(err))))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use quick_xml::de::from_reader;
|
use quick_xml::de::from_reader;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use http_body_util::BodyExt;
|
||||||
|
use hyper::{Request, Response, StatusCode};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::{ReqBody, ResBody};
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
|
use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
|
||||||
use crate::signature::verify_signed_content;
|
use crate::signature::verify_signed_content;
|
||||||
|
@ -16,7 +19,7 @@ use garage_model::bucket_table::{
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
|
||||||
pub async fn handle_get_lifecycle(bucket: &Bucket) -> Result<Response<Body>, Error> {
|
pub async fn handle_get_lifecycle(bucket: &Bucket) -> Result<Response<ResBody>, Error> {
|
||||||
let param = bucket
|
let param = bucket
|
||||||
.params()
|
.params()
|
||||||
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
||||||
|
@ -27,18 +30,18 @@ pub async fn handle_get_lifecycle(bucket: &Bucket) -> Result<Response<Body>, Err
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header(http::header::CONTENT_TYPE, "application/xml")
|
.header(http::header::CONTENT_TYPE, "application/xml")
|
||||||
.body(Body::from(xml))?)
|
.body(string_body(xml))?)
|
||||||
} else {
|
} else {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_delete_lifecycle(
|
pub async fn handle_delete_lifecycle(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
mut bucket: Bucket,
|
mut bucket: Bucket,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let param = bucket
|
let param = bucket
|
||||||
.params_mut()
|
.params_mut()
|
||||||
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
||||||
|
@ -48,16 +51,16 @@ pub async fn handle_delete_lifecycle(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_put_lifecycle(
|
pub async fn handle_put_lifecycle(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
mut bucket: Bucket,
|
mut bucket: Bucket,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
content_sha256: Option<Hash>,
|
content_sha256: Option<Hash>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
let body = BodyExt::collect(req.into_body()).await?.to_bytes();
|
||||||
|
|
||||||
if let Some(content_sha256) = content_sha256 {
|
if let Some(content_sha256) = content_sha256 {
|
||||||
verify_signed_content(content_sha256, &body[..])?;
|
verify_signed_content(content_sha256, &body[..])?;
|
||||||
|
@ -77,7 +80,7 @@ pub async fn handle_put_lifecycle(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- SERIALIZATION AND DESERIALIZATION TO/FROM S3 XML ----
|
// ---- SERIALIZATION AND DESERIALIZATION TO/FROM S3 XML ----
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::iter::{Iterator, Peekable};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
use hyper::{Body, Response};
|
use hyper::Response;
|
||||||
|
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
use garage_util::error::Error as GarageError;
|
use garage_util::error::Error as GarageError;
|
||||||
|
@ -16,7 +16,8 @@ use garage_model::s3::object_table::*;
|
||||||
use garage_table::EnumerationOrder;
|
use garage_table::EnumerationOrder;
|
||||||
|
|
||||||
use crate::encoding::*;
|
use crate::encoding::*;
|
||||||
use crate::helpers::key_after_prefix;
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::ResBody;
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::multipart as s3_multipart;
|
use crate::s3::multipart as s3_multipart;
|
||||||
use crate::s3::xml as s3_xml;
|
use crate::s3::xml as s3_xml;
|
||||||
|
@ -63,7 +64,7 @@ pub struct ListPartsQuery {
|
||||||
pub async fn handle_list(
|
pub async fn handle_list(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
query: &ListObjectsQuery,
|
query: &ListObjectsQuery,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let io = |bucket, key, count| {
|
let io = |bucket, key, count| {
|
||||||
let t = &garage.object_table;
|
let t = &garage.object_table;
|
||||||
async move {
|
async move {
|
||||||
|
@ -162,13 +163,13 @@ pub async fn handle_list(
|
||||||
let xml = s3_xml::to_xml_with_header(&result)?;
|
let xml = s3_xml::to_xml_with_header(&result)?;
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("Content-Type", "application/xml")
|
.header("Content-Type", "application/xml")
|
||||||
.body(Body::from(xml.into_bytes()))?)
|
.body(string_body(xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_list_multipart_upload(
|
pub async fn handle_list_multipart_upload(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
query: &ListMultipartUploadsQuery,
|
query: &ListMultipartUploadsQuery,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let io = |bucket, key, count| {
|
let io = |bucket, key, count| {
|
||||||
let t = &garage.object_table;
|
let t = &garage.object_table;
|
||||||
async move {
|
async move {
|
||||||
|
@ -264,13 +265,13 @@ pub async fn handle_list_multipart_upload(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("Content-Type", "application/xml")
|
.header("Content-Type", "application/xml")
|
||||||
.body(Body::from(xml.into_bytes()))?)
|
.body(string_body(xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_list_parts(
|
pub async fn handle_list_parts(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
query: &ListPartsQuery,
|
query: &ListPartsQuery,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
debug!("ListParts {:?}", query);
|
debug!("ListParts {:?}", query);
|
||||||
|
|
||||||
let upload_id = s3_multipart::decode_upload_id(&query.upload_id)?;
|
let upload_id = s3_multipart::decode_upload_id(&query.upload_id)?;
|
||||||
|
@ -319,7 +320,7 @@ pub async fn handle_list_parts(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("Content-Type", "application/xml")
|
.header("Content-Type", "application/xml")
|
||||||
.body(Body::from(xml.into_bytes()))?)
|
.body(string_body(xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -2,7 +2,6 @@ use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use hyper::body::Body;
|
|
||||||
use hyper::{Request, Response};
|
use hyper::{Request, Response};
|
||||||
use md5::{Digest as Md5Digest, Md5};
|
use md5::{Digest as Md5Digest, Md5};
|
||||||
|
|
||||||
|
@ -17,6 +16,8 @@ use garage_model::s3::mpu_table::*;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
use garage_model::s3::version_table::*;
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::{ReqBody, ResBody};
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::put::*;
|
use crate::s3::put::*;
|
||||||
use crate::s3::xml as s3_xml;
|
use crate::s3::xml as s3_xml;
|
||||||
|
@ -26,11 +27,11 @@ use crate::signature::verify_signed_content;
|
||||||
|
|
||||||
pub async fn handle_create_multipart_upload(
|
pub async fn handle_create_multipart_upload(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: &Request<Body>,
|
req: &Request<ReqBody>,
|
||||||
bucket_name: &str,
|
bucket_name: &str,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
key: &String,
|
key: &String,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let existing_object = garage.object_table.get(&bucket_id, &key).await?;
|
let existing_object = garage.object_table.get(&bucket_id, &key).await?;
|
||||||
|
|
||||||
let upload_id = gen_uuid();
|
let upload_id = gen_uuid();
|
||||||
|
@ -65,18 +66,18 @@ pub async fn handle_create_multipart_upload(
|
||||||
};
|
};
|
||||||
let xml = s3_xml::to_xml_with_header(&result)?;
|
let xml = s3_xml::to_xml_with_header(&result)?;
|
||||||
|
|
||||||
Ok(Response::new(Body::from(xml.into_bytes())))
|
Ok(Response::new(string_body(xml)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_put_part(
|
pub async fn handle_put_part(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
key: &str,
|
key: &str,
|
||||||
part_number: u64,
|
part_number: u64,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
content_sha256: Option<Hash>,
|
content_sha256: Option<Hash>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let upload_id = decode_upload_id(upload_id)?;
|
let upload_id = decode_upload_id(upload_id)?;
|
||||||
|
|
||||||
let content_md5 = match req.headers().get("content-md5") {
|
let content_md5 = match req.headers().get("content-md5") {
|
||||||
|
@ -87,8 +88,8 @@ pub async fn handle_put_part(
|
||||||
// Read first chuck, and at the same time try to get object to see if it exists
|
// 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 body = req.into_body().map_err(Error::from);
|
let stream = body_stream(req.into_body());
|
||||||
let mut chunker = StreamChunker::new(body, garage.config.block_size);
|
let mut chunker = StreamChunker::new(stream, garage.config.block_size);
|
||||||
|
|
||||||
let ((_, _, mut mpu), first_block) = futures::try_join!(
|
let ((_, _, mut mpu), first_block) = futures::try_join!(
|
||||||
get_upload(&garage, &bucket_id, &key, &upload_id),
|
get_upload(&garage, &bucket_id, &key, &upload_id),
|
||||||
|
@ -172,7 +173,7 @@ pub async fn handle_put_part(
|
||||||
|
|
||||||
let response = Response::builder()
|
let response = Response::builder()
|
||||||
.header("ETag", format!("\"{}\"", data_md5sum_hex))
|
.header("ETag", format!("\"{}\"", data_md5sum_hex))
|
||||||
.body(Body::empty())
|
.body(empty_body())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
@ -210,14 +211,16 @@ impl Drop for InterruptedCleanup {
|
||||||
|
|
||||||
pub async fn handle_complete_multipart_upload(
|
pub async fn handle_complete_multipart_upload(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
bucket_name: &str,
|
bucket_name: &str,
|
||||||
bucket: &Bucket,
|
bucket: &Bucket,
|
||||||
key: &str,
|
key: &str,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
content_sha256: Option<Hash>,
|
content_sha256: Option<Hash>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
let body = http_body_util::BodyExt::collect(req.into_body())
|
||||||
|
.await?
|
||||||
|
.to_bytes();
|
||||||
|
|
||||||
if let Some(content_sha256) = content_sha256 {
|
if let Some(content_sha256) = content_sha256 {
|
||||||
verify_signed_content(content_sha256, &body[..])?;
|
verify_signed_content(content_sha256, &body[..])?;
|
||||||
|
@ -365,7 +368,7 @@ pub async fn handle_complete_multipart_upload(
|
||||||
};
|
};
|
||||||
let xml = s3_xml::to_xml_with_header(&result)?;
|
let xml = s3_xml::to_xml_with_header(&result)?;
|
||||||
|
|
||||||
Ok(Response::new(Body::from(xml.into_bytes())))
|
Ok(Response::new(string_body(xml)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_abort_multipart_upload(
|
pub async fn handle_abort_multipart_upload(
|
||||||
|
@ -373,7 +376,7 @@ pub async fn handle_abort_multipart_upload(
|
||||||
bucket_id: Uuid,
|
bucket_id: Uuid,
|
||||||
key: &str,
|
key: &str,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let upload_id = decode_upload_id(upload_id)?;
|
let upload_id = decode_upload_id(upload_id)?;
|
||||||
|
|
||||||
let (_, mut object_version, _) =
|
let (_, mut object_version, _) =
|
||||||
|
@ -383,7 +386,7 @@ pub async fn handle_abort_multipart_upload(
|
||||||
let final_object = Object::new(bucket_id, key.to_string(), vec![object_version]);
|
let final_object = Object::new(bucket_id, key.to_string(), vec![object_version]);
|
||||||
garage.object_table.insert(&final_object).await?;
|
garage.object_table.insert(&final_object).await?;
|
||||||
|
|
||||||
Ok(Response::new(Body::from(vec![])))
|
Ok(Response::new(empty_body()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======== helpers ============
|
// ======== helpers ============
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::TryInto;
|
use std::convert::{Infallible, TryInto};
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
@ -9,12 +9,14 @@ use bytes::Bytes;
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
use hyper::header::{self, HeaderMap, HeaderName, HeaderValue};
|
use hyper::header::{self, HeaderMap, HeaderName, HeaderValue};
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
|
||||||
use multer::{Constraints, Multipart, SizeLimit};
|
use multer::{Constraints, Multipart, SizeLimit};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
|
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::ResBody;
|
||||||
use crate::s3::cors::*;
|
use crate::s3::cors::*;
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::put::{get_headers, save_stream};
|
use crate::s3::put::{get_headers, save_stream};
|
||||||
|
@ -23,9 +25,9 @@ use crate::signature::payload::{parse_date, verify_v4};
|
||||||
|
|
||||||
pub async fn handle_post_object(
|
pub async fn handle_post_object(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
bucket_name: String,
|
bucket_name: String,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let boundary = req
|
let boundary = req
|
||||||
.headers()
|
.headers()
|
||||||
.get(header::CONTENT_TYPE)
|
.get(header::CONTENT_TYPE)
|
||||||
|
@ -42,7 +44,8 @@ pub async fn handle_post_object(
|
||||||
);
|
);
|
||||||
|
|
||||||
let (head, body) = req.into_parts();
|
let (head, body) = req.into_parts();
|
||||||
let mut multipart = Multipart::with_constraints(body, boundary, constraints);
|
let stream = body_stream::<_, Error>(body);
|
||||||
|
let mut multipart = Multipart::with_constraints(stream, boundary, constraints);
|
||||||
|
|
||||||
let mut params = HeaderMap::new();
|
let mut params = HeaderMap::new();
|
||||||
let field = loop {
|
let field = loop {
|
||||||
|
@ -259,7 +262,7 @@ pub async fn handle_post_object(
|
||||||
.status(StatusCode::SEE_OTHER)
|
.status(StatusCode::SEE_OTHER)
|
||||||
.header(header::LOCATION, target.clone())
|
.header(header::LOCATION, target.clone())
|
||||||
.header(header::ETAG, etag)
|
.header(header::ETAG, etag)
|
||||||
.body(target.into())?
|
.body(string_body(target))?
|
||||||
} else {
|
} else {
|
||||||
let path = head
|
let path = head
|
||||||
.uri
|
.uri
|
||||||
|
@ -290,7 +293,7 @@ pub async fn handle_post_object(
|
||||||
.header(header::LOCATION, location.clone())
|
.header(header::LOCATION, location.clone())
|
||||||
.header(header::ETAG, etag.clone());
|
.header(header::ETAG, etag.clone());
|
||||||
match action {
|
match action {
|
||||||
"200" => builder.status(StatusCode::OK).body(Body::empty())?,
|
"200" => builder.status(StatusCode::OK).body(empty_body())?,
|
||||||
"201" => {
|
"201" => {
|
||||||
let xml = s3_xml::PostObject {
|
let xml = s3_xml::PostObject {
|
||||||
xmlns: (),
|
xmlns: (),
|
||||||
|
@ -302,14 +305,16 @@ pub async fn handle_post_object(
|
||||||
let body = s3_xml::to_xml_with_header(&xml)?;
|
let body = s3_xml::to_xml_with_header(&xml)?;
|
||||||
builder
|
builder
|
||||||
.status(StatusCode::CREATED)
|
.status(StatusCode::CREATED)
|
||||||
.body(Body::from(body.into_bytes()))?
|
.body(string_body(body))?
|
||||||
}
|
}
|
||||||
_ => builder.status(StatusCode::NO_CONTENT).body(Body::empty())?,
|
_ => builder.status(StatusCode::NO_CONTENT).body(empty_body())?,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let matching_cors_rule =
|
let matching_cors_rule = find_matching_cors_rule(
|
||||||
find_matching_cors_rule(&bucket, &Request::from_parts(head, Body::empty()))?;
|
&bucket,
|
||||||
|
&Request::from_parts(head, empty_body::<Infallible>()),
|
||||||
|
)?;
|
||||||
if let Some(rule) = matching_cors_rule {
|
if let Some(rule) = matching_cors_rule {
|
||||||
add_cors_headers(&mut resp, rule)
|
add_cors_headers(&mut resp, rule)
|
||||||
.ok_or_internal_error("Invalid bucket CORS configuration")?;
|
.ok_or_internal_error("Invalid bucket CORS configuration")?;
|
||||||
|
|
|
@ -4,11 +4,12 @@ use std::sync::Arc;
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use futures::try_join;
|
use futures::try_join;
|
||||||
|
use md5::{digest::generic_array::*, Digest as Md5Digest, Md5};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
use hyper::body::{Body, Bytes};
|
use hyper::body::{Body, Bytes};
|
||||||
use hyper::header::{HeaderMap, HeaderValue};
|
use hyper::header::{HeaderMap, HeaderValue};
|
||||||
use hyper::{Request, Response};
|
use hyper::{Request, Response};
|
||||||
use md5::{digest::generic_array::*, Digest as Md5Digest, Md5};
|
|
||||||
use sha2::Sha256;
|
|
||||||
|
|
||||||
use opentelemetry::{
|
use opentelemetry::{
|
||||||
trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer},
|
trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer},
|
||||||
|
@ -30,15 +31,17 @@ use garage_model::s3::block_ref_table::*;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
use garage_model::s3::version_table::*;
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::{ReqBody, ResBody};
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
|
|
||||||
pub async fn handle_put(
|
pub async fn handle_put(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
bucket: &Bucket,
|
bucket: &Bucket,
|
||||||
key: &String,
|
key: &String,
|
||||||
content_sha256: Option<Hash>,
|
content_sha256: Option<Hash>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
// Retrieve interesting headers from request
|
// Retrieve interesting headers from request
|
||||||
let headers = get_headers(req.headers())?;
|
let headers = get_headers(req.headers())?;
|
||||||
debug!("Object headers: {:?}", headers);
|
debug!("Object headers: {:?}", headers);
|
||||||
|
@ -48,13 +51,12 @@ pub async fn handle_put(
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (_head, body) = req.into_parts();
|
let stream = body_stream(req.into_body());
|
||||||
let body = body.map_err(Error::from);
|
|
||||||
|
|
||||||
save_stream(
|
save_stream(
|
||||||
garage,
|
garage,
|
||||||
headers,
|
headers,
|
||||||
body,
|
stream,
|
||||||
bucket,
|
bucket,
|
||||||
key,
|
key,
|
||||||
content_md5,
|
content_md5,
|
||||||
|
@ -434,11 +436,11 @@ impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put_response(version_uuid: Uuid, md5sum_hex: String) -> Response<Body> {
|
pub fn put_response(version_uuid: Uuid, md5sum_hex: String) -> Response<ResBody> {
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.header("x-amz-version-id", hex::encode(version_uuid))
|
.header("x-amz-version-id", hex::encode(version_uuid))
|
||||||
.header("ETag", format!("\"{}\"", md5sum_hex))
|
.header("ETag", format!("\"{}\"", md5sum_hex))
|
||||||
.body(Body::from(vec![]))
|
.body(empty_body())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
use quick_xml::de::from_reader;
|
use quick_xml::de::from_reader;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hyper::{Body, Request, Response, StatusCode};
|
use http_body_util::BodyExt;
|
||||||
|
use hyper::{Request, Response, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::helpers::*;
|
||||||
|
use crate::s3::api_server::{ReqBody, ResBody};
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
|
use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
|
||||||
use crate::signature::verify_signed_content;
|
use crate::signature::verify_signed_content;
|
||||||
|
@ -12,7 +15,7 @@ use garage_model::bucket_table::*;
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
|
||||||
pub async fn handle_get_website(bucket: &Bucket) -> Result<Response<Body>, Error> {
|
pub async fn handle_get_website(bucket: &Bucket) -> Result<Response<ResBody>, Error> {
|
||||||
let param = bucket
|
let param = bucket
|
||||||
.params()
|
.params()
|
||||||
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
||||||
|
@ -33,18 +36,18 @@ pub async fn handle_get_website(bucket: &Bucket) -> Result<Response<Body>, Error
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header(http::header::CONTENT_TYPE, "application/xml")
|
.header(http::header::CONTENT_TYPE, "application/xml")
|
||||||
.body(Body::from(xml))?)
|
.body(string_body(xml))?)
|
||||||
} else {
|
} else {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_delete_website(
|
pub async fn handle_delete_website(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
mut bucket: Bucket,
|
mut bucket: Bucket,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let param = bucket
|
let param = bucket
|
||||||
.params_mut()
|
.params_mut()
|
||||||
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
||||||
|
@ -54,16 +57,16 @@ pub async fn handle_delete_website(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_put_website(
|
pub async fn handle_put_website(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
mut bucket: Bucket,
|
mut bucket: Bucket,
|
||||||
req: Request<Body>,
|
req: Request<ReqBody>,
|
||||||
content_sha256: Option<Hash>,
|
content_sha256: Option<Hash>,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<ResBody>, Error> {
|
||||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
let body = BodyExt::collect(req.into_body()).await?.to_bytes();
|
||||||
|
|
||||||
if let Some(content_sha256) = content_sha256 {
|
if let Some(content_sha256) = content_sha256 {
|
||||||
verify_signed_content(content_sha256, &body[..])?;
|
verify_signed_content(content_sha256, &body[..])?;
|
||||||
|
@ -83,7 +86,7 @@ pub async fn handle_put_website(
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(Body::empty())?)
|
.body(empty_body())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
|
|
@ -18,10 +18,6 @@ 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 client sent a header with invalid value
|
|
||||||
#[error(display = "Invalid header value: {}", _0)]
|
|
||||||
InvalidHeader(#[error(source)] hyper::header::ToStrError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<T> for Error
|
impl<T> From<T> for Error
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc};
|
||||||
use hmac::Mac;
|
use hmac::Mac;
|
||||||
use hyper::{Body, Method, Request};
|
use hyper::{body::Incoming as IncomingBody, Method, Request};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use garage_table::*;
|
use garage_table::*;
|
||||||
|
@ -20,7 +20,7 @@ use crate::signature::error::*;
|
||||||
pub async fn check_payload_signature(
|
pub async fn check_payload_signature(
|
||||||
garage: &Garage,
|
garage: &Garage,
|
||||||
service: &'static str,
|
service: &'static str,
|
||||||
request: &Request<Body>,
|
request: &Request<IncomingBody>,
|
||||||
) -> Result<(Option<Key>, Option<Hash>), Error> {
|
) -> Result<(Option<Key>, Option<Hash>), Error> {
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
for (key, val) in request.headers() {
|
for (key, val) in request.headers() {
|
||||||
|
@ -316,7 +316,7 @@ fn canonical_query_string(uri: &hyper::Uri) -> String {
|
||||||
pub fn parse_date(date: &str) -> Result<DateTime<Utc>, Error> {
|
pub fn parse_date(date: &str) -> Result<DateTime<Utc>, Error> {
|
||||||
let date: NaiveDateTime =
|
let date: NaiveDateTime =
|
||||||
NaiveDateTime::parse_from_str(date, LONG_DATETIME).ok_or_bad_request("Invalid date")?;
|
NaiveDateTime::parse_from_str(date, LONG_DATETIME).ok_or_bad_request("Invalid date")?;
|
||||||
Ok(DateTime::from_utc(date, Utc))
|
Ok(Utc.from_utc_datetime(&date))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn verify_v4(
|
pub async fn verify_v4(
|
||||||
|
|
|
@ -1,26 +1,30 @@
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use chrono::{DateTime, NaiveDateTime, 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 garage_model::key_table::Key;
|
||||||
use hmac::Mac;
|
use hmac::Mac;
|
||||||
use hyper::body::Bytes;
|
use http_body_util::StreamBody;
|
||||||
use hyper::{Body, Request};
|
use hyper::body::{Bytes, Incoming as IncomingBody};
|
||||||
|
use hyper::Request;
|
||||||
|
|
||||||
use garage_util::data::Hash;
|
use garage_util::data::Hash;
|
||||||
|
|
||||||
use super::{compute_scope, sha256sum, HmacSha256, LONG_DATETIME};
|
use super::{compute_scope, sha256sum, HmacSha256, LONG_DATETIME};
|
||||||
|
|
||||||
|
use crate::helpers::*;
|
||||||
use crate::signature::error::*;
|
use crate::signature::error::*;
|
||||||
|
|
||||||
|
pub type ReqBody = BoxBody<Error>;
|
||||||
|
|
||||||
pub fn parse_streaming_body(
|
pub fn parse_streaming_body(
|
||||||
api_key: &Key,
|
api_key: &Key,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
content_sha256: &mut Option<Hash>,
|
content_sha256: &mut Option<Hash>,
|
||||||
region: &str,
|
region: &str,
|
||||||
service: &str,
|
service: &str,
|
||||||
) -> Result<Request<Body>, Error> {
|
) -> Result<Request<ReqBody>, Error> {
|
||||||
match req.headers().get("x-amz-content-sha256") {
|
match req.headers().get("x-amz-content-sha256") {
|
||||||
Some(header) if header == "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" => {
|
Some(header) if header == "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" => {
|
||||||
let signature = content_sha256
|
let signature = content_sha256
|
||||||
|
@ -40,26 +44,22 @@ pub fn parse_streaming_body(
|
||||||
.to_str()?;
|
.to_str()?;
|
||||||
let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, LONG_DATETIME)
|
let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, LONG_DATETIME)
|
||||||
.ok_or_bad_request("Invalid date")?;
|
.ok_or_bad_request("Invalid date")?;
|
||||||
let date: DateTime<Utc> = DateTime::from_utc(date, Utc);
|
let date: DateTime<Utc> = Utc.from_utc_datetime(&date);
|
||||||
|
|
||||||
let scope = compute_scope(&date, region, service);
|
let scope = compute_scope(&date, region, service);
|
||||||
let signing_hmac = crate::signature::signing_hmac(&date, secret_key, region, service)
|
let signing_hmac = crate::signature::signing_hmac(&date, secret_key, region, service)
|
||||||
.ok_or_internal_error("Unable to build signing HMAC")?;
|
.ok_or_internal_error("Unable to build signing HMAC")?;
|
||||||
|
|
||||||
Ok(req.map(move |body| {
|
Ok(req.map(move |body| {
|
||||||
Body::wrap_stream(
|
let stream = body_stream::<_, Error>(body);
|
||||||
SignedPayloadStream::new(
|
let signed_payload_stream =
|
||||||
body.map_err(Error::from),
|
SignedPayloadStream::new(stream, signing_hmac, date, &scope, signature)
|
||||||
signing_hmac,
|
.map(|x| x.map(hyper::body::Frame::data))
|
||||||
date,
|
.map_err(Error::from);
|
||||||
&scope,
|
ReqBody::new(StreamBody::new(signed_payload_stream))
|
||||||
signature,
|
|
||||||
)
|
|
||||||
.map_err(Error::from),
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
_ => Ok(req),
|
_ => Ok(req.map(|body| ReqBody::new(http_body_util::BodyExt::map_err(body, Error::from)))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,26 +19,26 @@ garage_rpc.workspace = true
|
||||||
garage_util.workspace = true
|
garage_util.workspace = true
|
||||||
garage_table.workspace = true
|
garage_table.workspace = true
|
||||||
|
|
||||||
opentelemetry = "0.17"
|
opentelemetry.workspace = true
|
||||||
|
|
||||||
arc-swap = "1.5"
|
arc-swap.workspace = true
|
||||||
async-trait = "0.1.7"
|
async-trait.workspace = true
|
||||||
bytes = "1.0"
|
bytes.workspace = true
|
||||||
bytesize = "1.2"
|
bytesize.workspace = true
|
||||||
hex = "0.4"
|
hex.workspace = true
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
rand = "0.8"
|
rand.workspace = true
|
||||||
|
|
||||||
async-compression = { version = "0.4", features = ["tokio", "zstd"] }
|
async-compression.workspace = true
|
||||||
zstd = { version = "0.12", default-features = false }
|
zstd.workspace = true
|
||||||
|
|
||||||
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
|
serde.workspace = true
|
||||||
serde_bytes = "0.11"
|
serde_bytes.workspace = true
|
||||||
|
|
||||||
futures = "0.3"
|
futures.workspace = true
|
||||||
futures-util = "0.3"
|
futures-util.workspace = true
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio.workspace = true
|
||||||
tokio-util = { version = "0.7", features = ["io"] }
|
tokio-util.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
system-libs = [ "zstd/pkg-config" ]
|
system-libs = [ "zstd/pkg-config" ]
|
||||||
|
|
|
@ -53,6 +53,9 @@ pub const INLINE_THRESHOLD: usize = 3072;
|
||||||
// to delete the block locally.
|
// to delete the block locally.
|
||||||
pub(crate) const BLOCK_GC_DELAY: Duration = Duration::from_secs(600);
|
pub(crate) const BLOCK_GC_DELAY: Duration = Duration::from_secs(600);
|
||||||
|
|
||||||
|
pub type BlockStream =
|
||||||
|
Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send + Sync + 'static>>;
|
||||||
|
|
||||||
/// RPC messages used to share blocks of data between nodes
|
/// RPC messages used to share blocks of data between nodes
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum BlockRpc {
|
pub enum BlockRpc {
|
||||||
|
@ -324,10 +327,7 @@ impl BlockManager {
|
||||||
&self,
|
&self,
|
||||||
hash: &Hash,
|
hash: &Hash,
|
||||||
order_tag: Option<OrderTag>,
|
order_tag: Option<OrderTag>,
|
||||||
) -> Result<
|
) -> Result<BlockStream, Error> {
|
||||||
Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send + Sync + 'static>>,
|
|
||||||
Error,
|
|
||||||
> {
|
|
||||||
let (header, stream) = self.rpc_get_raw_block_streaming(hash, order_tag).await?;
|
let (header, stream) = self.rpc_get_raw_block_streaming(hash, order_tag).await?;
|
||||||
match header {
|
match header {
|
||||||
DataBlockHeader::Plain => Ok(stream),
|
DataBlockHeader::Plain => Ok(stream),
|
||||||
|
|
|
@ -12,24 +12,19 @@ readme = "../../README.md"
|
||||||
path = "lib.rs"
|
path = "lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
err-derive = "0.3"
|
err-derive.workspace = true
|
||||||
hexdump = "0.1"
|
hexdump.workspace = true
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
|
|
||||||
heed = { version = "0.11", default-features = false, features = ["lmdb"], optional = true }
|
heed = { workspace = true, optional = true }
|
||||||
rusqlite = { version = "0.29", optional = true }
|
rusqlite = { workspace = true, optional = true }
|
||||||
sled = { version = "0.34", optional = true }
|
sled = { workspace = true, optional = true }
|
||||||
|
|
||||||
# cli deps
|
|
||||||
clap = { version = "4.1", optional = true, features = ["derive", "env"] }
|
|
||||||
pretty_env_logger = { version = "0.5", optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mktemp = "0.5"
|
mktemp.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "sled", "lmdb", "sqlite" ]
|
default = [ "sled", "lmdb", "sqlite" ]
|
||||||
bundled-libs = [ "rusqlite?/bundled" ]
|
bundled-libs = [ "rusqlite?/bundled" ]
|
||||||
cli = ["clap", "pretty_env_logger"]
|
|
||||||
lmdb = [ "heed" ]
|
lmdb = [ "heed" ]
|
||||||
sqlite = [ "rusqlite" ]
|
sqlite = [ "rusqlite" ]
|
||||||
|
|
|
@ -31,49 +31,51 @@ garage_table.workspace = true
|
||||||
garage_util.workspace = true
|
garage_util.workspace = true
|
||||||
garage_web.workspace = true
|
garage_web.workspace = true
|
||||||
|
|
||||||
backtrace = "0.3"
|
backtrace.workspace = true
|
||||||
bytes = "1.0"
|
bytes.workspace = true
|
||||||
bytesize = "1.2"
|
bytesize.workspace = true
|
||||||
timeago = { version = "0.4", default-features = false }
|
timeago.workspace = true
|
||||||
parse_duration = "2.1"
|
parse_duration.workspace = true
|
||||||
hex = "0.4"
|
hex.workspace = true
|
||||||
tracing = { version = "0.1" }
|
tracing.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber.workspace = true
|
||||||
rand = "0.8"
|
rand.workspace = true
|
||||||
async-trait = "0.1.7"
|
async-trait.workspace = true
|
||||||
sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" }
|
sodiumoxide.workspace = true
|
||||||
git-version = "0.3.4"
|
structopt.workspace = true
|
||||||
|
git-version.workspace = true
|
||||||
|
|
||||||
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
|
serde.workspace = true
|
||||||
serde_bytes = "0.11"
|
serde_bytes.workspace = true
|
||||||
structopt = { version = "0.3", default-features = false }
|
toml.workspace = true
|
||||||
toml = "0.6"
|
|
||||||
|
|
||||||
futures = "0.3"
|
futures.workspace = true
|
||||||
futures-util = "0.3"
|
futures-util.workspace = true
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio.workspace = true
|
||||||
|
|
||||||
netapp = "0.10"
|
netapp.workspace = true
|
||||||
|
|
||||||
opentelemetry = { version = "0.17", features = [ "rt-tokio" ] }
|
opentelemetry.workspace = true
|
||||||
opentelemetry-prometheus = { version = "0.10", optional = true }
|
opentelemetry-prometheus = { workspace = true, optional = true }
|
||||||
opentelemetry-otlp = { version = "0.10", optional = true }
|
opentelemetry-otlp = { workspace = true, optional = true }
|
||||||
prometheus = { version = "0.13", optional = true }
|
prometheus = { workspace = true, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
aws-config = "0.55.2"
|
aws-config.workspace = true
|
||||||
aws-sdk-s3 = "0.28"
|
aws-sdk-s3.workspace = true
|
||||||
chrono = "0.4"
|
chrono.workspace = true
|
||||||
http = "0.2"
|
http.workspace = true
|
||||||
hmac = "0.12"
|
hmac.workspace = true
|
||||||
hyper = { version = "0.14", features = ["client", "http1", "runtime"] }
|
http-body-util.workspace = true
|
||||||
mktemp = "0.5"
|
hyper.workspace = true
|
||||||
sha2 = "0.10"
|
hyper-util.workspace = true
|
||||||
|
mktemp.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
|
||||||
static_init = "1.0"
|
static_init.workspace = true
|
||||||
assert-json-diff = "2.0"
|
assert-json-diff.workspace = true
|
||||||
serde_json = "1.0"
|
serde_json.workspace = true
|
||||||
base64 = "0.21"
|
base64.workspace = true
|
||||||
|
|
||||||
k2v-client.workspace = true
|
k2v-client.workspace = true
|
||||||
|
|
||||||
|
|
|
@ -113,12 +113,11 @@ pub async fn run_server(config_file: PathBuf, secrets: Secrets) -> Result<(), Er
|
||||||
|
|
||||||
if let Some(web_config) = &config.s3_web {
|
if let Some(web_config) = &config.s3_web {
|
||||||
info!("Initializing web server...");
|
info!("Initializing web server...");
|
||||||
|
let web_server = WebServer::new(garage.clone(), web_config.root_domain.clone());
|
||||||
servers.push((
|
servers.push((
|
||||||
"Web",
|
"Web",
|
||||||
tokio::spawn(WebServer::run(
|
tokio::spawn(web_server.run(
|
||||||
garage.clone(),
|
|
||||||
web_config.bind_addr.clone(),
|
web_config.bind_addr.clone(),
|
||||||
web_config.root_domain.clone(),
|
|
||||||
wait_from(watch_cancel.clone()),
|
wait_from(watch_cancel.clone()),
|
||||||
)),
|
)),
|
||||||
));
|
));
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use aws_sdk_s3::config::BehaviorVersion;
|
||||||
use aws_sdk_s3::config::Credentials;
|
use aws_sdk_s3::config::Credentials;
|
||||||
use aws_sdk_s3::{Client, Config};
|
use aws_sdk_s3::{Client, Config};
|
||||||
|
|
||||||
|
@ -11,6 +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())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Client::from_conf(config)
|
Client::from_conf(config)
|
||||||
|
|
|
@ -5,12 +5,17 @@ use std::convert::TryFrom;
|
||||||
|
|
||||||
use chrono::{offset::Utc, DateTime};
|
use chrono::{offset::Utc, DateTime};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use hyper::client::HttpConnector;
|
use http_body_util::BodyExt;
|
||||||
use hyper::{Body, Client, Method, Request, Response, Uri};
|
use http_body_util::Full as FullBody;
|
||||||
|
use hyper::{Method, Request, Response, Uri};
|
||||||
|
use hyper_util::client::legacy::{connect::HttpConnector, Client};
|
||||||
|
use hyper_util::rt::TokioExecutor;
|
||||||
|
|
||||||
use super::garage::{Instance, Key};
|
use super::garage::{Instance, Key};
|
||||||
use garage_api::signature;
|
use garage_api::signature;
|
||||||
|
|
||||||
|
pub type Body = FullBody<hyper::body::Bytes>;
|
||||||
|
|
||||||
/// You should ever only use this to send requests AWS sdk won't send,
|
/// You should ever only use this to send requests AWS sdk won't send,
|
||||||
/// like to reproduce behavior of unusual implementations found to be
|
/// like to reproduce behavior of unusual implementations found to be
|
||||||
/// problematic.
|
/// problematic.
|
||||||
|
@ -19,7 +24,7 @@ pub struct CustomRequester {
|
||||||
key: Key,
|
key: Key,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
service: &'static str,
|
service: &'static str,
|
||||||
client: Client<HttpConnector>,
|
client: Client<HttpConnector, Body>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomRequester {
|
impl CustomRequester {
|
||||||
|
@ -28,7 +33,7 @@ impl CustomRequester {
|
||||||
key: key.clone(),
|
key: key.clone(),
|
||||||
uri: instance.s3_uri(),
|
uri: instance.s3_uri(),
|
||||||
service: "s3",
|
service: "s3",
|
||||||
client: Client::new(),
|
client: Client::builder(TokioExecutor::new()).build_http(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +42,7 @@ impl CustomRequester {
|
||||||
key: key.clone(),
|
key: key.clone(),
|
||||||
uri: instance.k2v_uri(),
|
uri: instance.k2v_uri(),
|
||||||
service: "k2v",
|
service: "k2v",
|
||||||
client: Client::new(),
|
client: Client::builder(TokioExecutor::new()).build_http(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +144,7 @@ impl<'a> RequestBuilder<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send(&mut self) -> hyper::Result<Response<Body>> {
|
pub async fn send(&mut self) -> Result<Response<Body>, String> {
|
||||||
// TODO this is a bit incorrect in that path and query params should be url-encoded and
|
// TODO this is a bit incorrect in that path and query params should be url-encoded and
|
||||||
// aren't, but this is good enought for now.
|
// aren't, but this is good enought for now.
|
||||||
|
|
||||||
|
@ -242,7 +247,22 @@ impl<'a> RequestBuilder<'a> {
|
||||||
.method(self.method.clone())
|
.method(self.method.clone())
|
||||||
.body(Body::from(body))
|
.body(Body::from(body))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.requester.client.request(request).await
|
|
||||||
|
let result = self
|
||||||
|
.requester
|
||||||
|
.client
|
||||||
|
.request(request)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("hyper client error: {}", err))?;
|
||||||
|
|
||||||
|
let (head, body) = result.into_parts();
|
||||||
|
let body = Body::new(
|
||||||
|
body.collect()
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("hyper client error in body.collect: {}", err))?
|
||||||
|
.to_bytes(),
|
||||||
|
);
|
||||||
|
Ok(Response::from_parts(head, body))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ use base64::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::json_body;
|
use crate::json_body;
|
||||||
|
use http_body_util::BodyExt;
|
||||||
use hyper::{Method, StatusCode};
|
use hyper::{Method, StatusCode};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -77,10 +78,7 @@ async fn test_batch() {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
let res_body = hyper::body::to_bytes(res.into_body())
|
let res_body = res.into_body().collect().await.unwrap().to_bytes();
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.to_vec();
|
|
||||||
assert_eq!(res_body, values.get(sk).unwrap().as_bytes());
|
assert_eq!(res_body, values.get(sk).unwrap().as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ use base64::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::json_body;
|
use crate::json_body;
|
||||||
|
use http_body_util::BodyExt;
|
||||||
use hyper::{Method, StatusCode};
|
use hyper::{Method, StatusCode};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -83,10 +84,7 @@ async fn test_items_and_indices() {
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string();
|
.to_string();
|
||||||
let res_body = hyper::body::to_bytes(res.into_body())
|
let res_body = res.into_body().collect().await.unwrap().to_bytes();
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.to_vec();
|
|
||||||
assert_eq!(res_body, content);
|
assert_eq!(res_body, content);
|
||||||
|
|
||||||
// ReadIndex -- now there should be some stuff
|
// ReadIndex -- now there should be some stuff
|
||||||
|
@ -152,10 +150,7 @@ async fn test_items_and_indices() {
|
||||||
res.headers().get("content-type").unwrap().to_str().unwrap(),
|
res.headers().get("content-type").unwrap().to_str().unwrap(),
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
);
|
);
|
||||||
let res_body = hyper::body::to_bytes(res.into_body())
|
let res_body = res.into_body().collect().await.unwrap().to_bytes();
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.to_vec();
|
|
||||||
assert_eq!(res_body, content2);
|
assert_eq!(res_body, content2);
|
||||||
|
|
||||||
// ReadIndex -- now there should be some stuff
|
// ReadIndex -- now there should be some stuff
|
||||||
|
@ -394,10 +389,7 @@ async fn test_item_return_format() {
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string();
|
.to_string();
|
||||||
let res_body = hyper::body::to_bytes(res.into_body())
|
let res_body = res.into_body().collect().await.unwrap().to_bytes();
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.to_vec();
|
|
||||||
assert_eq!(res_body, single_value);
|
assert_eq!(res_body, single_value);
|
||||||
|
|
||||||
// f1: not specified
|
// f1: not specified
|
||||||
|
@ -434,10 +426,7 @@ async fn test_item_return_format() {
|
||||||
res.headers().get("content-type").unwrap().to_str().unwrap(),
|
res.headers().get("content-type").unwrap().to_str().unwrap(),
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
);
|
);
|
||||||
let res_body = hyper::body::to_bytes(res.into_body())
|
let res_body = res.into_body().collect().await.unwrap().to_bytes();
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.to_vec();
|
|
||||||
assert_eq!(res_body, single_value);
|
assert_eq!(res_body, single_value);
|
||||||
|
|
||||||
// f3: json
|
// f3: json
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
|
use http_body_util::BodyExt;
|
||||||
use hyper::{Method, StatusCode};
|
use hyper::{Method, StatusCode};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -47,11 +48,8 @@ async fn test_poll_item() {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let res2_body = hyper::body::to_bytes(res2.into_body())
|
let res2_body = res2.into_body().collect().await.unwrap().to_bytes();
|
||||||
.await
|
assert_eq!(res2_body, b"Initial value"[..]);
|
||||||
.unwrap()
|
|
||||||
.to_vec();
|
|
||||||
assert_eq!(res2_body, b"Initial value");
|
|
||||||
|
|
||||||
// Start poll operation
|
// Start poll operation
|
||||||
let poll = {
|
let poll = {
|
||||||
|
@ -95,11 +93,8 @@ async fn test_poll_item() {
|
||||||
|
|
||||||
assert_eq!(poll_res.status(), StatusCode::OK);
|
assert_eq!(poll_res.status(), StatusCode::OK);
|
||||||
|
|
||||||
let poll_res_body = hyper::body::to_bytes(poll_res.into_body())
|
let poll_res_body = poll_res.into_body().collect().await.unwrap().to_bytes();
|
||||||
.await
|
assert_eq!(poll_res_body, b"New value"[..]);
|
||||||
.unwrap()
|
|
||||||
.to_vec();
|
|
||||||
assert_eq!(poll_res_body, b"New value");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::common;
|
use crate::common;
|
||||||
|
|
||||||
|
use http_body_util::BodyExt;
|
||||||
use hyper::{Method, StatusCode};
|
use hyper::{Method, StatusCode};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -32,9 +33,6 @@ async fn test_simple() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res2.status(), StatusCode::OK);
|
assert_eq!(res2.status(), StatusCode::OK);
|
||||||
|
|
||||||
let res2_body = hyper::body::to_bytes(res2.into_body())
|
let res2_body = res2.into_body().collect().await.unwrap().to_bytes();
|
||||||
.await
|
assert_eq!(res2_body, b"Hello, world!"[..]);
|
||||||
.unwrap()
|
|
||||||
.to_vec();
|
|
||||||
assert_eq!(res2_body, b"Hello, world!");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,15 @@ mod k2v;
|
||||||
#[cfg(feature = "k2v")]
|
#[cfg(feature = "k2v")]
|
||||||
mod k2v_client;
|
mod k2v_client;
|
||||||
|
|
||||||
use hyper::{Body, Response};
|
use http_body_util::BodyExt;
|
||||||
|
use hyper::{body::Body, Response};
|
||||||
|
|
||||||
pub async fn json_body(res: Response<Body>) -> serde_json::Value {
|
pub async fn json_body<B>(res: Response<B>) -> serde_json::Value
|
||||||
let res_body: serde_json::Value = serde_json::from_slice(
|
where
|
||||||
&hyper::body::to_bytes(res.into_body())
|
B: Body,
|
||||||
.await
|
<B as Body>::Error: std::fmt::Debug,
|
||||||
.unwrap()
|
{
|
||||||
.to_vec()[..],
|
let body = res.into_body().collect().await.unwrap().to_bytes();
|
||||||
)
|
let res_body: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||||
.unwrap();
|
|
||||||
res_body
|
res_body
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,7 +154,7 @@ async fn test_multipart_upload() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(r.content_length, (SZ_5MB * 3) as i64);
|
assert_eq!(r.content_length.unwrap(), (SZ_5MB * 3) as i64);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -183,7 +183,7 @@ async fn test_multipart_upload() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
eprintln!("get_object with part_number = {}", part_number);
|
eprintln!("get_object with part_number = {}", part_number);
|
||||||
assert_eq!(o.content_length, SZ_5MB as i64);
|
assert_eq!(o.content_length.unwrap(), SZ_5MB as i64);
|
||||||
assert_bytes_eq!(o.body, data);
|
assert_bytes_eq!(o.body, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,14 +249,14 @@ async fn test_uploadlistpart() {
|
||||||
|
|
||||||
let ps = r.parts.unwrap();
|
let ps = r.parts.unwrap();
|
||||||
assert_eq!(ps.len(), 1);
|
assert_eq!(ps.len(), 1);
|
||||||
assert_eq!(ps[0].part_number, 2);
|
assert_eq!(ps[0].part_number.unwrap(), 2);
|
||||||
let fp = &ps[0];
|
let fp = &ps[0];
|
||||||
assert!(fp.last_modified.is_some());
|
assert!(fp.last_modified.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fp.e_tag.as_ref().unwrap(),
|
fp.e_tag.as_ref().unwrap(),
|
||||||
"\"3366bb9dcf710d6801b5926467d02e19\""
|
"\"3366bb9dcf710d6801b5926467d02e19\""
|
||||||
);
|
);
|
||||||
assert_eq!(fp.size, SZ_5MB as i64);
|
assert_eq!(fp.size.unwrap(), SZ_5MB as i64);
|
||||||
}
|
}
|
||||||
|
|
||||||
let p2 = ctx
|
let p2 = ctx
|
||||||
|
@ -286,23 +286,23 @@ async fn test_uploadlistpart() {
|
||||||
let ps = r.parts.unwrap();
|
let ps = r.parts.unwrap();
|
||||||
assert_eq!(ps.len(), 2);
|
assert_eq!(ps.len(), 2);
|
||||||
|
|
||||||
assert_eq!(ps[0].part_number, 1);
|
assert_eq!(ps[0].part_number.unwrap(), 1);
|
||||||
let fp = &ps[0];
|
let fp = &ps[0];
|
||||||
assert!(fp.last_modified.is_some());
|
assert!(fp.last_modified.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fp.e_tag.as_ref().unwrap(),
|
fp.e_tag.as_ref().unwrap(),
|
||||||
"\"3c484266f9315485694556e6c693bfa2\""
|
"\"3c484266f9315485694556e6c693bfa2\""
|
||||||
);
|
);
|
||||||
assert_eq!(fp.size, SZ_5MB as i64);
|
assert_eq!(fp.size.unwrap(), SZ_5MB as i64);
|
||||||
|
|
||||||
assert_eq!(ps[1].part_number, 2);
|
assert_eq!(ps[1].part_number.unwrap(), 2);
|
||||||
let sp = &ps[1];
|
let sp = &ps[1];
|
||||||
assert!(sp.last_modified.is_some());
|
assert!(sp.last_modified.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sp.e_tag.as_ref().unwrap(),
|
sp.e_tag.as_ref().unwrap(),
|
||||||
"\"3366bb9dcf710d6801b5926467d02e19\""
|
"\"3366bb9dcf710d6801b5926467d02e19\""
|
||||||
);
|
);
|
||||||
assert_eq!(sp.size, SZ_5MB as i64);
|
assert_eq!(sp.size.unwrap(), SZ_5MB as i64);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -320,14 +320,14 @@ async fn test_uploadlistpart() {
|
||||||
|
|
||||||
assert!(r.part_number_marker.is_none());
|
assert!(r.part_number_marker.is_none());
|
||||||
assert_eq!(r.next_part_number_marker.as_deref(), Some("1"));
|
assert_eq!(r.next_part_number_marker.as_deref(), Some("1"));
|
||||||
assert_eq!(r.max_parts, 1_i32);
|
assert_eq!(r.max_parts.unwrap(), 1_i32);
|
||||||
assert!(r.is_truncated);
|
assert!(r.is_truncated.unwrap());
|
||||||
assert_eq!(r.key.unwrap(), "a");
|
assert_eq!(r.key.unwrap(), "a");
|
||||||
assert_eq!(r.upload_id.unwrap().as_str(), uid.as_str());
|
assert_eq!(r.upload_id.unwrap().as_str(), uid.as_str());
|
||||||
let parts = r.parts.unwrap();
|
let parts = r.parts.unwrap();
|
||||||
assert_eq!(parts.len(), 1);
|
assert_eq!(parts.len(), 1);
|
||||||
let fp = &parts[0];
|
let fp = &parts[0];
|
||||||
assert_eq!(fp.part_number, 1);
|
assert_eq!(fp.part_number.unwrap(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fp.e_tag.as_ref().unwrap(),
|
fp.e_tag.as_ref().unwrap(),
|
||||||
"\"3c484266f9315485694556e6c693bfa2\""
|
"\"3c484266f9315485694556e6c693bfa2\""
|
||||||
|
@ -349,19 +349,19 @@ async fn test_uploadlistpart() {
|
||||||
r2.part_number_marker.as_ref().unwrap(),
|
r2.part_number_marker.as_ref().unwrap(),
|
||||||
r.next_part_number_marker.as_ref().unwrap()
|
r.next_part_number_marker.as_ref().unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(r2.max_parts, 1_i32);
|
assert_eq!(r2.max_parts.unwrap(), 1_i32);
|
||||||
assert_eq!(r2.key.unwrap(), "a");
|
assert_eq!(r2.key.unwrap(), "a");
|
||||||
assert_eq!(r2.upload_id.unwrap().as_str(), uid.as_str());
|
assert_eq!(r2.upload_id.unwrap().as_str(), uid.as_str());
|
||||||
let parts = r2.parts.unwrap();
|
let parts = r2.parts.unwrap();
|
||||||
assert_eq!(parts.len(), 1);
|
assert_eq!(parts.len(), 1);
|
||||||
let fp = &parts[0];
|
let fp = &parts[0];
|
||||||
assert_eq!(fp.part_number, 2);
|
assert_eq!(fp.part_number.unwrap(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fp.e_tag.as_ref().unwrap(),
|
fp.e_tag.as_ref().unwrap(),
|
||||||
"\"3366bb9dcf710d6801b5926467d02e19\""
|
"\"3366bb9dcf710d6801b5926467d02e19\""
|
||||||
);
|
);
|
||||||
//assert!(r2.is_truncated); // WHY? (this was the test before)
|
//assert!(r2.is_truncated); // WHY? (this was the test before)
|
||||||
assert!(!r2.is_truncated);
|
assert!(!r2.is_truncated.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmp = CompletedMultipartUpload::builder()
|
let cmp = CompletedMultipartUpload::builder()
|
||||||
|
@ -411,7 +411,7 @@ async fn test_uploadlistpart() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(r.content_length, (SZ_5MB * 2) as i64);
|
assert_eq!(r.content_length.unwrap(), (SZ_5MB * 2) as i64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,9 +50,9 @@ async fn test_putobject() {
|
||||||
// assert_eq!(o.version_id.unwrap(), _version);
|
// assert_eq!(o.version_id.unwrap(), _version);
|
||||||
assert_eq!(o.content_type.unwrap(), content_type);
|
assert_eq!(o.content_type.unwrap(), content_type);
|
||||||
assert!(o.last_modified.is_some());
|
assert!(o.last_modified.is_some());
|
||||||
assert_eq!(o.content_length, 0);
|
assert_eq!(o.content_length.unwrap(), 0);
|
||||||
assert_eq!(o.parts_count, 0);
|
assert_eq!(o.parts_count, None);
|
||||||
assert_eq!(o.tag_count, 0);
|
assert_eq!(o.tag_count, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -86,9 +86,9 @@ async fn test_putobject() {
|
||||||
assert_bytes_eq!(o.body, b"hi");
|
assert_bytes_eq!(o.body, b"hi");
|
||||||
assert_eq!(o.e_tag.unwrap(), etag);
|
assert_eq!(o.e_tag.unwrap(), etag);
|
||||||
assert!(o.last_modified.is_some());
|
assert!(o.last_modified.is_some());
|
||||||
assert_eq!(o.content_length, 2);
|
assert_eq!(o.content_length.unwrap(), 2);
|
||||||
assert_eq!(o.parts_count, 0);
|
assert_eq!(o.parts_count, None);
|
||||||
assert_eq!(o.tag_count, 0);
|
assert_eq!(o.tag_count, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -119,9 +119,9 @@ async fn test_putobject() {
|
||||||
assert_bytes_eq!(o.body, b"");
|
assert_bytes_eq!(o.body, b"");
|
||||||
assert_eq!(o.e_tag.unwrap(), etag);
|
assert_eq!(o.e_tag.unwrap(), etag);
|
||||||
assert!(o.last_modified.is_some());
|
assert!(o.last_modified.is_some());
|
||||||
assert_eq!(o.content_length, 0);
|
assert_eq!(o.content_length.unwrap(), 0);
|
||||||
assert_eq!(o.parts_count, 0);
|
assert_eq!(o.parts_count, None);
|
||||||
assert_eq!(o.tag_count, 0);
|
assert_eq!(o.tag_count, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ async fn test_deleteobject() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
to_del = to_del.objects(ObjectIdentifier::builder().key(k).build());
|
to_del = to_del.objects(ObjectIdentifier::builder().key(k).build().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +223,7 @@ async fn test_deleteobject() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
to_del = to_del.objects(ObjectIdentifier::builder().key(k).build());
|
to_del = to_del.objects(ObjectIdentifier::builder().key(k).build().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,7 +247,7 @@ async fn test_deleteobject() {
|
||||||
.client
|
.client
|
||||||
.delete_objects()
|
.delete_objects()
|
||||||
.bucket(&bucket)
|
.bucket(&bucket)
|
||||||
.delete(to_del.build())
|
.delete(to_del.build().unwrap())
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -57,9 +57,9 @@ async fn test_putobject_streaming() {
|
||||||
// assert_eq!(o.version_id.unwrap(), _version);
|
// assert_eq!(o.version_id.unwrap(), _version);
|
||||||
assert_eq!(o.content_type.unwrap(), content_type);
|
assert_eq!(o.content_type.unwrap(), content_type);
|
||||||
assert!(o.last_modified.is_some());
|
assert!(o.last_modified.is_some());
|
||||||
assert_eq!(o.content_length, 0);
|
assert_eq!(o.content_length.unwrap(), 0);
|
||||||
assert_eq!(o.parts_count, 0);
|
assert_eq!(o.parts_count, None);
|
||||||
assert_eq!(o.tag_count, 0);
|
assert_eq!(o.tag_count, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -95,9 +95,9 @@ async fn test_putobject_streaming() {
|
||||||
assert_bytes_eq!(o.body, BODY);
|
assert_bytes_eq!(o.body, BODY);
|
||||||
assert_eq!(o.e_tag.unwrap(), etag);
|
assert_eq!(o.e_tag.unwrap(), etag);
|
||||||
assert!(o.last_modified.is_some());
|
assert!(o.last_modified.is_some());
|
||||||
assert_eq!(o.content_length, 62);
|
assert_eq!(o.content_length.unwrap(), 62);
|
||||||
assert_eq!(o.parts_count, 0);
|
assert_eq!(o.parts_count, None);
|
||||||
assert_eq!(o.tag_count, 0);
|
assert_eq!(o.tag_count, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +187,7 @@ async fn test_put_website_streaming() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(o.index_document.unwrap().suffix.unwrap(), "home.html");
|
assert_eq!(o.index_document.unwrap().suffix, "home.html");
|
||||||
assert_eq!(o.error_document.unwrap().key.unwrap(), "err/error.html");
|
assert_eq!(o.error_document.unwrap().key, "err/error.html");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,18 @@ use aws_sdk_s3::{
|
||||||
types::{CorsConfiguration, CorsRule, ErrorDocument, IndexDocument, WebsiteConfiguration},
|
types::{CorsConfiguration, CorsRule, ErrorDocument, IndexDocument, WebsiteConfiguration},
|
||||||
};
|
};
|
||||||
use http::{Request, StatusCode};
|
use http::{Request, StatusCode};
|
||||||
use hyper::{
|
use http_body_util::BodyExt;
|
||||||
body::{to_bytes, Body},
|
use http_body_util::Full as FullBody;
|
||||||
Client,
|
use hyper::body::Bytes;
|
||||||
};
|
use hyper_util::client::legacy::Client;
|
||||||
|
use hyper_util::rt::TokioExecutor;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
const BODY: &[u8; 16] = b"<h1>bonjour</h1>";
|
const BODY: &[u8; 16] = b"<h1>bonjour</h1>";
|
||||||
const BODY_ERR: &[u8; 6] = b"erreur";
|
const BODY_ERR: &[u8; 6] = b"erreur";
|
||||||
|
|
||||||
|
pub type Body = FullBody<Bytes>;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_website() {
|
async fn test_website() {
|
||||||
const BCKT_NAME: &str = "my-website";
|
const BCKT_NAME: &str = "my-website";
|
||||||
|
@ -34,14 +37,14 @@ async fn test_website() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let client = Client::new();
|
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||||
|
|
||||||
let req = || {
|
let req = || {
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("GET")
|
.method("GET")
|
||||||
.uri(format!("http://127.0.0.1:{}/", ctx.garage.web_port))
|
.uri(format!("http://127.0.0.1:{}/", ctx.garage.web_port))
|
||||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ async fn test_website() {
|
||||||
|
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
BodyExt::collect(resp.into_body()).await.unwrap().to_bytes(),
|
||||||
BODY.as_ref()
|
BODY.as_ref()
|
||||||
); /* check that we do not leak body */
|
); /* check that we do not leak body */
|
||||||
|
|
||||||
|
@ -61,7 +64,7 @@ async fn test_website() {
|
||||||
ctx.garage.admin_port,
|
ctx.garage.admin_port,
|
||||||
BCKT_NAME.to_string()
|
BCKT_NAME.to_string()
|
||||||
))
|
))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -87,7 +90,7 @@ async fn test_website() {
|
||||||
resp = client.request(req()).await.unwrap();
|
resp = client.request(req()).await.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
resp.into_body().collect().await.unwrap().to_bytes(),
|
||||||
BODY.as_ref()
|
BODY.as_ref()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -103,14 +106,14 @@ async fn test_website() {
|
||||||
"http://127.0.0.1:{0}/check?domain={1}",
|
"http://127.0.0.1:{0}/check?domain={1}",
|
||||||
ctx.garage.admin_port, bname
|
ctx.garage.admin_port, bname
|
||||||
))
|
))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut admin_resp = client.request(admin_req()).await.unwrap();
|
let admin_resp = client.request(admin_req()).await.unwrap();
|
||||||
assert_eq!(admin_resp.status(), StatusCode::OK);
|
assert_eq!(admin_resp.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_bytes(admin_resp.body_mut()).await.unwrap().as_ref(),
|
admin_resp.into_body().collect().await.unwrap().to_bytes(),
|
||||||
format!("Domain '{bname}' is managed by Garage").as_bytes()
|
format!("Domain '{bname}' is managed by Garage").as_bytes()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -124,7 +127,7 @@ async fn test_website() {
|
||||||
resp = client.request(req()).await.unwrap();
|
resp = client.request(req()).await.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
resp.into_body().collect().await.unwrap().to_bytes(),
|
||||||
BODY.as_ref()
|
BODY.as_ref()
|
||||||
); /* check that we do not leak body */
|
); /* check that we do not leak body */
|
||||||
|
|
||||||
|
@ -136,7 +139,7 @@ async fn test_website() {
|
||||||
ctx.garage.admin_port,
|
ctx.garage.admin_port,
|
||||||
BCKT_NAME.to_string()
|
BCKT_NAME.to_string()
|
||||||
))
|
))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -181,8 +184,18 @@ async fn test_website_s3_api() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let conf = WebsiteConfiguration::builder()
|
let conf = WebsiteConfiguration::builder()
|
||||||
.index_document(IndexDocument::builder().suffix("home.html").build())
|
.index_document(
|
||||||
.error_document(ErrorDocument::builder().key("err/error.html").build())
|
IndexDocument::builder()
|
||||||
|
.suffix("home.html")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.error_document(
|
||||||
|
ErrorDocument::builder()
|
||||||
|
.key("err/error.html")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
ctx.client
|
ctx.client
|
||||||
|
@ -201,9 +214,11 @@ async fn test_website_s3_api() {
|
||||||
.allowed_methods("GET")
|
.allowed_methods("GET")
|
||||||
.allowed_methods("PUT")
|
.allowed_methods("PUT")
|
||||||
.allowed_origins("*")
|
.allowed_origins("*")
|
||||||
.build(),
|
.build()
|
||||||
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.build();
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
ctx.client
|
ctx.client
|
||||||
.put_bucket_cors()
|
.put_bucket_cors()
|
||||||
|
@ -222,24 +237,21 @@ async fn test_website_s3_api() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let main_rule = cors_res.cors_rules().unwrap().iter().next().unwrap();
|
let main_rule = cors_res.cors_rules().iter().next().unwrap();
|
||||||
|
|
||||||
assert_eq!(main_rule.id.as_ref().unwrap(), "main-rule");
|
assert_eq!(main_rule.id.as_ref().unwrap(), "main-rule");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
main_rule.allowed_headers.as_ref().unwrap(),
|
main_rule.allowed_headers.as_ref().unwrap(),
|
||||||
&vec!["*".to_string()]
|
&vec!["*".to_string()]
|
||||||
);
|
);
|
||||||
|
assert_eq!(&main_rule.allowed_origins, &vec!["*".to_string()]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
main_rule.allowed_origins.as_ref().unwrap(),
|
&main_rule.allowed_methods,
|
||||||
&vec!["*".to_string()]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
main_rule.allowed_methods.as_ref().unwrap(),
|
|
||||||
&vec!["GET".to_string(), "PUT".to_string()]
|
&vec!["GET".to_string(), "PUT".to_string()]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = Client::new();
|
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||||
|
|
||||||
// Test direct requests with CORS
|
// Test direct requests with CORS
|
||||||
{
|
{
|
||||||
|
@ -248,10 +260,10 @@ async fn test_website_s3_api() {
|
||||||
.uri(format!("http://127.0.0.1:{}/site/", ctx.garage.web_port))
|
.uri(format!("http://127.0.0.1:{}/site/", ctx.garage.web_port))
|
||||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||||
.header("Origin", "https://example.com")
|
.header("Origin", "https://example.com")
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut resp = client.request(req).await.unwrap();
|
let resp = client.request(req).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -259,7 +271,7 @@ async fn test_website_s3_api() {
|
||||||
"*"
|
"*"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
resp.into_body().collect().await.unwrap().to_bytes(),
|
||||||
BODY.as_ref()
|
BODY.as_ref()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -273,14 +285,14 @@ async fn test_website_s3_api() {
|
||||||
ctx.garage.web_port
|
ctx.garage.web_port
|
||||||
))
|
))
|
||||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut resp = client.request(req).await.unwrap();
|
let resp = client.request(req).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
resp.into_body().collect().await.unwrap().to_bytes(),
|
||||||
BODY_ERR.as_ref()
|
BODY_ERR.as_ref()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -293,10 +305,10 @@ async fn test_website_s3_api() {
|
||||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||||
.header("Origin", "https://example.com")
|
.header("Origin", "https://example.com")
|
||||||
.header("Access-Control-Request-Method", "PUT")
|
.header("Access-Control-Request-Method", "PUT")
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut resp = client.request(req).await.unwrap();
|
let resp = client.request(req).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -304,7 +316,7 @@ async fn test_website_s3_api() {
|
||||||
"*"
|
"*"
|
||||||
);
|
);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
resp.into_body().collect().await.unwrap().to_bytes(),
|
||||||
BODY.as_ref()
|
BODY.as_ref()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -317,14 +329,14 @@ async fn test_website_s3_api() {
|
||||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||||
.header("Origin", "https://example.com")
|
.header("Origin", "https://example.com")
|
||||||
.header("Access-Control-Request-Method", "DELETE")
|
.header("Access-Control-Request-Method", "DELETE")
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut resp = client.request(req).await.unwrap();
|
let resp = client.request(req).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
resp.into_body().collect().await.unwrap().to_bytes(),
|
||||||
BODY.as_ref()
|
BODY.as_ref()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -358,14 +370,14 @@ async fn test_website_s3_api() {
|
||||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||||
.header("Origin", "https://example.com")
|
.header("Origin", "https://example.com")
|
||||||
.header("Access-Control-Request-Method", "PUT")
|
.header("Access-Control-Request-Method", "PUT")
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut resp = client.request(req).await.unwrap();
|
let resp = client.request(req).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
resp.into_body().collect().await.unwrap().to_bytes(),
|
||||||
BODY.as_ref()
|
BODY.as_ref()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -384,20 +396,15 @@ async fn test_website_s3_api() {
|
||||||
.method("GET")
|
.method("GET")
|
||||||
.uri(format!("http://127.0.0.1:{}/site/", ctx.garage.web_port))
|
.uri(format!("http://127.0.0.1:{}/site/", ctx.garage.web_port))
|
||||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut resp = client.request(req).await.unwrap();
|
let resp = client.request(req).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
assert_ne!(
|
let resp_bytes = resp.into_body().collect().await.unwrap().to_bytes();
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
assert_ne!(resp_bytes, BODY_ERR.as_ref());
|
||||||
BODY_ERR.as_ref()
|
assert_ne!(resp_bytes, BODY.as_ref());
|
||||||
);
|
|
||||||
assert_ne!(
|
|
||||||
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
|
|
||||||
BODY.as_ref()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,13 +412,13 @@ async fn test_website_s3_api() {
|
||||||
async fn test_website_check_domain() {
|
async fn test_website_check_domain() {
|
||||||
let ctx = common::context();
|
let ctx = common::context();
|
||||||
|
|
||||||
let client = Client::new();
|
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||||
|
|
||||||
let admin_req = || {
|
let admin_req = || {
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("GET")
|
.method("GET")
|
||||||
.uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
|
.uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -435,7 +442,7 @@ async fn test_website_check_domain() {
|
||||||
"http://127.0.0.1:{}/check?domain=",
|
"http://127.0.0.1:{}/check?domain=",
|
||||||
ctx.garage.admin_port
|
ctx.garage.admin_port
|
||||||
))
|
))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -459,7 +466,7 @@ async fn test_website_check_domain() {
|
||||||
"http://127.0.0.1:{}/check?domain=foobar",
|
"http://127.0.0.1:{}/check?domain=foobar",
|
||||||
ctx.garage.admin_port
|
ctx.garage.admin_port
|
||||||
))
|
))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -483,7 +490,7 @@ async fn test_website_check_domain() {
|
||||||
"http://127.0.0.1:{}/check?domain=%E2%98%B9",
|
"http://127.0.0.1:{}/check?domain=%E2%98%B9",
|
||||||
ctx.garage.admin_port
|
ctx.garage.admin_port
|
||||||
))
|
))
|
||||||
.body(Body::empty())
|
.body(Body::new(Bytes::new()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,25 +9,28 @@ repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
|
||||||
readme = "../../README.md"
|
readme = "../../README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.21"
|
base64.workspace = true
|
||||||
sha2 = "0.10"
|
sha2.workspace = true
|
||||||
hex = "0.4"
|
hex.workspace = true
|
||||||
http = "0.2"
|
http.workspace = true
|
||||||
log = "0.4"
|
http-body-util.workspace = true
|
||||||
aws-sigv4 = "0.55"
|
log.workspace = true
|
||||||
percent-encoding = "2.2"
|
aws-sigv4.workspace = true
|
||||||
hyper = { version = "0.14", default-features = false, features = ["client", "http1", "http2"] }
|
aws-sdk-config.workspace = true
|
||||||
hyper-rustls = { version = "0.24", features = ["http2"] }
|
percent-encoding.workspace = true
|
||||||
serde = { version = "1.0", features = [ "derive" ] }
|
hyper = { workspace = true, default-features = false, features = ["http1", "http2"] }
|
||||||
serde_json = "1.0"
|
hyper-util.workspace = true
|
||||||
thiserror = "1.0"
|
hyper-rustls.workspace = true
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
# cli deps
|
# cli deps
|
||||||
clap = { version = "4.1", optional = true, features = ["derive", "env"] }
|
clap = { workspace = true, optional = true }
|
||||||
format_table = { workspace = true, optional = true }
|
format_table = { workspace = true, optional = true }
|
||||||
tracing = { version = "0.1", optional = true }
|
tracing = { workspace = true, optional = true }
|
||||||
tracing-subscriber = { version = "0.3", optional = true, features = ["env-filter"] }
|
tracing-subscriber = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -22,12 +22,14 @@ pub enum Error {
|
||||||
Http(#[from] http::Error),
|
Http(#[from] http::Error),
|
||||||
#[error("hyper error: {0}")]
|
#[error("hyper error: {0}")]
|
||||||
Hyper(#[from] hyper::Error),
|
Hyper(#[from] hyper::Error),
|
||||||
|
#[error("hyper client error: {0}")]
|
||||||
|
HyperClient(#[from] hyper_util::client::legacy::Error),
|
||||||
#[error("invalid header: {0}")]
|
#[error("invalid header: {0}")]
|
||||||
Header(#[from] hyper::header::ToStrError),
|
Header(#[from] hyper::header::ToStrError),
|
||||||
#[error("deserialization error: {0}")]
|
#[error("deserialization error: {0}")]
|
||||||
Deserialization(#[from] serde_json::Error),
|
Deserialization(#[from] serde_json::Error),
|
||||||
#[error("invalid signature parameters: {0}")]
|
#[error("invalid signature parameters: {0}")]
|
||||||
SignParameters(#[from] aws_sigv4::signing_params::BuildError),
|
SignParameters(#[from] aws_sigv4::sign::v4::signing_params::BuildError),
|
||||||
#[error("could not sign request: {0}")]
|
#[error("could not sign request: {0}")]
|
||||||
SignRequest(#[from] aws_sigv4::http_request::SigningError),
|
SignRequest(#[from] aws_sigv4::http_request::SigningError),
|
||||||
#[error("request timed out")]
|
#[error("request timed out")]
|
||||||
|
|
|
@ -9,11 +9,15 @@ use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||||
use http::header::{ACCEPT, CONTENT_TYPE};
|
use http::header::{ACCEPT, CONTENT_TYPE};
|
||||||
use http::status::StatusCode;
|
use http::status::StatusCode;
|
||||||
use http::{HeaderName, HeaderValue, Request};
|
use http::{HeaderName, HeaderValue, Request};
|
||||||
use hyper::{body::Bytes, Body};
|
use http_body_util::{BodyExt, Full as FullBody};
|
||||||
use hyper::{client::connect::HttpConnector, Client as HttpClient};
|
use hyper::{body::Body as BodyTrait, body::Bytes};
|
||||||
use hyper_rustls::HttpsConnector;
|
use hyper_rustls::HttpsConnector;
|
||||||
|
use hyper_util::client::legacy::{connect::HttpConnector, Client as HttpClient};
|
||||||
|
use hyper_util::rt::TokioExecutor;
|
||||||
|
|
||||||
use aws_sigv4::http_request::{sign, SignableRequest, SigningParams, SigningSettings};
|
use aws_sdk_config::config::Credentials;
|
||||||
|
use aws_sigv4::http_request::{sign, SignableBody, SignableRequest, SigningSettings};
|
||||||
|
use aws_sigv4::sign::v4::SigningParams;
|
||||||
|
|
||||||
use serde::de::Error as DeError;
|
use serde::de::Error as DeError;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
@ -22,6 +26,8 @@ mod error;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
|
pub type Body = FullBody<Bytes>;
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
|
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_secs(300);
|
const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_secs(300);
|
||||||
const SERVICE: &str = "k2v";
|
const SERVICE: &str = "k2v";
|
||||||
|
@ -53,19 +59,19 @@ pub struct K2vClientConfig {
|
||||||
pub struct K2vClient {
|
pub struct K2vClient {
|
||||||
config: K2vClientConfig,
|
config: K2vClientConfig,
|
||||||
user_agent: HeaderValue,
|
user_agent: HeaderValue,
|
||||||
client: HttpClient<HttpsConnector<HttpConnector>>,
|
client: HttpClient<HttpsConnector<HttpConnector>, Body>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl K2vClient {
|
impl K2vClient {
|
||||||
/// Create a new K2V client.
|
/// Create a new K2V client.
|
||||||
pub fn new(config: K2vClientConfig) -> Result<Self, Error> {
|
pub fn new(config: K2vClientConfig) -> Result<Self, Error> {
|
||||||
let connector = hyper_rustls::HttpsConnectorBuilder::new()
|
let connector = hyper_rustls::HttpsConnectorBuilder::new()
|
||||||
.with_native_roots()
|
.with_native_roots()?
|
||||||
.https_or_http()
|
.https_or_http()
|
||||||
.enable_http1()
|
.enable_http1()
|
||||||
.enable_http2()
|
.enable_http2()
|
||||||
.build();
|
.build();
|
||||||
let client = HttpClient::builder().build(connector);
|
let client = HttpClient::builder(TokioExecutor::new()).build(connector);
|
||||||
let user_agent: std::borrow::Cow<str> = match &config.user_agent {
|
let user_agent: std::borrow::Cow<str> = match &config.user_agent {
|
||||||
Some(ua) => ua.into(),
|
Some(ua) => ua.into(),
|
||||||
None => format!("k2v/{}", env!("CARGO_PKG_VERSION")).into(),
|
None => format!("k2v/{}", env!("CARGO_PKG_VERSION")).into(),
|
||||||
|
@ -363,21 +369,37 @@ impl K2vClient {
|
||||||
|
|
||||||
// Sign request
|
// Sign request
|
||||||
let signing_settings = SigningSettings::default();
|
let signing_settings = SigningSettings::default();
|
||||||
|
let identity = Credentials::new(
|
||||||
|
&self.config.aws_access_key_id,
|
||||||
|
&self.config.aws_secret_access_key,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"k2v-client",
|
||||||
|
)
|
||||||
|
.into();
|
||||||
let signing_params = SigningParams::builder()
|
let signing_params = SigningParams::builder()
|
||||||
.access_key(&self.config.aws_access_key_id)
|
.identity(&identity)
|
||||||
.secret_key(&self.config.aws_secret_access_key)
|
|
||||||
.region(&self.config.region)
|
.region(&self.config.region)
|
||||||
.service_name(SERVICE)
|
.name(SERVICE)
|
||||||
.time(SystemTime::now())
|
.time(SystemTime::now())
|
||||||
.settings(signing_settings)
|
.settings(signing_settings)
|
||||||
.build()?;
|
.build()?
|
||||||
|
.into();
|
||||||
// Convert the HTTP request into a signable request
|
// Convert the HTTP request into a signable request
|
||||||
let signable_request = SignableRequest::from(&req);
|
let signable_request = SignableRequest::new(
|
||||||
|
req.method().as_str(),
|
||||||
|
req.uri().to_string(),
|
||||||
|
// TODO: get rid of Unwrap
|
||||||
|
req.headers()
|
||||||
|
.iter()
|
||||||
|
.map(|(x, y)| (x.as_str(), y.to_str().unwrap())),
|
||||||
|
SignableBody::Bytes(req.body().as_ref()),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Sign and then apply the signature to the request
|
// Sign and then apply the signature to the request
|
||||||
let (signing_instructions, _signature) =
|
let (signing_instructions, _signature) =
|
||||||
sign(signable_request, &signing_params)?.into_parts();
|
sign(signable_request, &signing_params)?.into_parts();
|
||||||
signing_instructions.apply_to_request(&mut req);
|
signing_instructions.apply_to_request_http1x(&mut req);
|
||||||
|
|
||||||
// Send and wait for timeout
|
// Send and wait for timeout
|
||||||
let res = tokio::select! {
|
let res = tokio::select! {
|
||||||
|
@ -398,12 +420,16 @@ impl K2vClient {
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = match res.status {
|
let body = match res.status {
|
||||||
StatusCode::OK => hyper::body::to_bytes(body).await?,
|
StatusCode::OK => BodyExt::collect(body).await?.to_bytes(),
|
||||||
StatusCode::NO_CONTENT => Bytes::new(),
|
StatusCode::NO_CONTENT => Bytes::new(),
|
||||||
StatusCode::NOT_FOUND => return Err(Error::NotFound),
|
StatusCode::NOT_FOUND => return Err(Error::NotFound),
|
||||||
StatusCode::NOT_MODIFIED => Bytes::new(),
|
StatusCode::NOT_MODIFIED => Bytes::new(),
|
||||||
s => {
|
s => {
|
||||||
let err_body = hyper::body::to_bytes(body).await.unwrap_or_default();
|
let err_body = body
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.map(|x| x.to_bytes())
|
||||||
|
.unwrap_or_default();
|
||||||
let err_body_str = std::str::from_utf8(&err_body)
|
let err_body_str = std::str::from_utf8(&err_body)
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
.unwrap_or_else(|_| BASE64_STANDARD.encode(&err_body));
|
.unwrap_or_else(|_| BASE64_STANDARD.encode(&err_body));
|
||||||
|
@ -451,7 +477,11 @@ impl K2vClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_url<V: AsRef<str>>(&self, partition_key: Option<&str>, query: &[(&str, V)]) -> String {
|
fn build_url<V: AsRef<str>>(&self, partition_key: Option<&str>, query: &[(&str, V)]) -> String {
|
||||||
let mut url = format!("{}/{}", self.config.endpoint, self.config.bucket);
|
let mut url = format!(
|
||||||
|
"{}/{}",
|
||||||
|
self.config.endpoint.trim_end_matches('/'),
|
||||||
|
self.config.bucket
|
||||||
|
);
|
||||||
if let Some(pk) = partition_key {
|
if let Some(pk) = partition_key {
|
||||||
url.push('/');
|
url.push('/');
|
||||||
url.extend(utf8_percent_encode(pk, &PATH_ENCODE_SET));
|
url.extend(utf8_percent_encode(pk, &PATH_ENCODE_SET));
|
||||||
|
|
|
@ -20,26 +20,26 @@ garage_table.workspace = true
|
||||||
garage_block.workspace = true
|
garage_block.workspace = true
|
||||||
garage_util.workspace = true
|
garage_util.workspace = true
|
||||||
|
|
||||||
async-trait = "0.1.7"
|
async-trait.workspace = true
|
||||||
arc-swap = "1.0"
|
arc-swap.workspace = true
|
||||||
blake2 = "0.10"
|
blake2.workspace = true
|
||||||
chrono = "0.4"
|
chrono.workspace = true
|
||||||
err-derive = "0.3"
|
err-derive.workspace = true
|
||||||
hex = "0.4"
|
hex.workspace = true
|
||||||
base64 = "0.21"
|
base64.workspace = true
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
rand = "0.8"
|
rand.workspace = true
|
||||||
zstd = { version = "0.12", default-features = false }
|
zstd.workspace = true
|
||||||
|
|
||||||
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
|
serde.workspace = true
|
||||||
serde_bytes = "0.11"
|
serde_bytes.workspace = true
|
||||||
|
|
||||||
futures = "0.3"
|
futures.workspace = true
|
||||||
futures-util = "0.3"
|
futures-util.workspace = true
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio.workspace = true
|
||||||
opentelemetry = "0.17"
|
opentelemetry.workspace = true
|
||||||
|
|
||||||
netapp = "0.10"
|
netapp.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "sled", "lmdb", "sqlite" ]
|
default = [ "sled", "lmdb", "sqlite" ]
|
||||||
|
|
|
@ -18,38 +18,38 @@ format_table.workspace = true
|
||||||
garage_db.workspace = true
|
garage_db.workspace = true
|
||||||
garage_util.workspace = true
|
garage_util.workspace = true
|
||||||
|
|
||||||
arc-swap = "1.0"
|
arc-swap.workspace = true
|
||||||
bytes = "1.0"
|
bytes.workspace = true
|
||||||
bytesize = "1.1"
|
bytesize.workspace = true
|
||||||
gethostname = "0.4"
|
gethostname.workspace = true
|
||||||
hex = "0.4"
|
hex.workspace = true
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
rand = "0.8"
|
rand.workspace = true
|
||||||
itertools="0.10"
|
itertools.workspace = true
|
||||||
sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" }
|
sodiumoxide.workspace = true
|
||||||
nix = { version = "0.27", default-features = false, features = ["fs"] }
|
nix.workspace = true
|
||||||
|
|
||||||
async-trait = "0.1.7"
|
async-trait.workspace = true
|
||||||
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
|
serde.workspace = true
|
||||||
serde_bytes = "0.11"
|
serde_bytes.workspace = true
|
||||||
serde_json = "1.0"
|
serde_json.workspace = true
|
||||||
err-derive = { version = "0.3", optional = true }
|
err-derive = { workspace = true, optional = true }
|
||||||
|
|
||||||
# newer version requires rust edition 2021
|
# newer version requires rust edition 2021
|
||||||
kube = { version = "0.75", default-features = false, features = ["runtime", "derive", "client", "rustls-tls"], optional = true }
|
kube = { workspace = true, optional = true }
|
||||||
k8s-openapi = { version = "0.16", features = ["v1_22"], optional = true }
|
k8s-openapi = { workspace = true, optional = true }
|
||||||
schemars = { version = "0.8", optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
reqwest = { version = "0.11", optional = true, default-features = false, features = ["rustls-tls-manual-roots", "json"] }
|
reqwest = { workspace = true, optional = true }
|
||||||
|
|
||||||
pnet_datalink = "0.33"
|
pnet_datalink.workspace = true
|
||||||
|
|
||||||
futures = "0.3"
|
futures.workspace = true
|
||||||
futures-util = "0.3"
|
futures-util.workspace = true
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio.workspace = true
|
||||||
tokio-stream = { version = "0.1", features = ["net"] }
|
tokio-stream.workspace = true
|
||||||
opentelemetry = "0.17"
|
opentelemetry.workspace = true
|
||||||
|
|
||||||
netapp = { version = "0.10", features = ["telemetry"] }
|
netapp.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
kubernetes-discovery = [ "kube", "k8s-openapi", "schemars" ]
|
kubernetes-discovery = [ "kube", "k8s-openapi", "schemars" ]
|
||||||
|
|
|
@ -18,19 +18,19 @@ garage_db.workspace = true
|
||||||
garage_rpc.workspace = true
|
garage_rpc.workspace = true
|
||||||
garage_util.workspace = true
|
garage_util.workspace = true
|
||||||
|
|
||||||
opentelemetry = "0.17"
|
opentelemetry.workspace = true
|
||||||
|
|
||||||
async-trait = "0.1.7"
|
async-trait.workspace = true
|
||||||
arc-swap = "1.0"
|
arc-swap.workspace = true
|
||||||
bytes = "1.0"
|
bytes.workspace = true
|
||||||
hex = "0.4"
|
hex.workspace = true
|
||||||
hexdump = "0.1"
|
hexdump.workspace = true
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
rand = "0.8"
|
rand.workspace = true
|
||||||
|
|
||||||
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
|
serde.workspace = true
|
||||||
serde_bytes = "0.11"
|
serde_bytes.workspace = true
|
||||||
|
|
||||||
futures = "0.3"
|
futures.workspace = true
|
||||||
futures-util = "0.3"
|
futures-util.workspace = true
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio.workspace = true
|
||||||
|
|
|
@ -16,42 +16,42 @@ path = "lib.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
garage_db.workspace = true
|
garage_db.workspace = true
|
||||||
|
|
||||||
arc-swap = "1.0"
|
arc-swap.workspace = true
|
||||||
async-trait = "0.1"
|
async-trait.workspace = true
|
||||||
blake2 = "0.10"
|
blake2.workspace = true
|
||||||
bytes = "1.0"
|
bytes.workspace = true
|
||||||
bytesize = "1.2"
|
bytesize.workspace = true
|
||||||
digest = "0.10"
|
digest.workspace = true
|
||||||
err-derive = "0.3"
|
err-derive.workspace = true
|
||||||
hexdump = "0.1"
|
hexdump.workspace = true
|
||||||
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
|
xxhash-rust.workspace = true
|
||||||
hex = "0.4"
|
hex.workspace = true
|
||||||
lazy_static = "1.4"
|
lazy_static.workspace = true
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
rand = "0.8"
|
rand.workspace = true
|
||||||
sha2 = "0.10"
|
sha2.workspace = true
|
||||||
|
|
||||||
chrono = "0.4"
|
chrono.workspace = true
|
||||||
rmp-serde = "1.1.2"
|
rmp-serde.workspace = true
|
||||||
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
|
serde.workspace = true
|
||||||
serde_json = "1.0"
|
serde_json.workspace = true
|
||||||
toml = "0.6"
|
toml.workspace = true
|
||||||
|
|
||||||
futures = "0.3"
|
futures.workspace = true
|
||||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio.workspace = true
|
||||||
|
|
||||||
netapp = "0.10"
|
netapp.workspace = true
|
||||||
|
|
||||||
http = "0.2"
|
http.workspace = true
|
||||||
hyper = "0.14"
|
hyper.workspace = true
|
||||||
|
|
||||||
opentelemetry = { version = "0.17", features = [ "rt-tokio", "metrics", "trace" ] }
|
opentelemetry.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
rustc_version = "0.4.0"
|
rustc_version.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mktemp = "0.5"
|
mktemp.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
k2v = []
|
k2v = []
|
||||||
|
|
|
@ -19,16 +19,17 @@ garage_model.workspace = true
|
||||||
garage_util.workspace = true
|
garage_util.workspace = true
|
||||||
garage_table.workspace = true
|
garage_table.workspace = true
|
||||||
|
|
||||||
err-derive = "0.3"
|
err-derive.workspace = true
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding.workspace = true
|
||||||
|
|
||||||
futures = "0.3"
|
futures.workspace = true
|
||||||
|
|
||||||
http = "0.2"
|
http.workspace = true
|
||||||
hyper = { version = "0.14", features = ["server", "http1", "runtime", "tcp", "stream"] }
|
http-body-util.workspace = true
|
||||||
hyperlocal = { version = "0.8.0", default-features = false, features = ["server"] }
|
hyper.workspace = true
|
||||||
|
hyper-util.workspace = true
|
||||||
|
|
||||||
tokio = { version = "1.0", default-features = false, features = ["net"] }
|
tokio.workspace = true
|
||||||
|
|
||||||
opentelemetry = "0.17"
|
opentelemetry.workspace = true
|
||||||
|
|
|
@ -5,15 +5,12 @@ use std::{convert::Infallible, sync::Arc};
|
||||||
use futures::future::Future;
|
use futures::future::Future;
|
||||||
|
|
||||||
use hyper::{
|
use hyper::{
|
||||||
|
body::Incoming as IncomingBody,
|
||||||
header::{HeaderValue, HOST},
|
header::{HeaderValue, HOST},
|
||||||
server::conn::AddrStream,
|
Method, Request, Response, StatusCode,
|
||||||
service::{make_service_fn, service_fn},
|
|
||||||
Body, Method, Request, Response, Server, StatusCode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use hyperlocal::UnixServerExt;
|
use tokio::net::{TcpListener, UnixListener};
|
||||||
|
|
||||||
use tokio::net::UnixStream;
|
|
||||||
|
|
||||||
use opentelemetry::{
|
use opentelemetry::{
|
||||||
global,
|
global,
|
||||||
|
@ -24,7 +21,8 @@ use opentelemetry::{
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
|
|
||||||
use garage_api::helpers::{authority_to_host, host_to_bucket};
|
use garage_api::generic_server::{server_loop, UnixListenerOn};
|
||||||
|
use garage_api::helpers::*;
|
||||||
use garage_api::s3::cors::{add_cors_headers, find_matching_cors_rule, handle_options_for_bucket};
|
use garage_api::s3::cors::{add_cors_headers, find_matching_cors_rule, handle_options_for_bucket};
|
||||||
use garage_api::s3::error::{
|
use garage_api::s3::error::{
|
||||||
CommonErrorDerivative, Error as ApiError, OkOrBadRequest, OkOrInternalError,
|
CommonErrorDerivative, Error as ApiError, OkOrBadRequest, OkOrInternalError,
|
||||||
|
@ -74,78 +72,52 @@ pub struct WebServer {
|
||||||
|
|
||||||
impl WebServer {
|
impl WebServer {
|
||||||
/// Run a web server
|
/// Run a web server
|
||||||
pub async fn run(
|
pub fn new(garage: Arc<Garage>, root_domain: String) -> Arc<Self> {
|
||||||
garage: Arc<Garage>,
|
|
||||||
addr: UnixOrTCPSocketAddress,
|
|
||||||
root_domain: String,
|
|
||||||
shutdown_signal: impl Future<Output = ()>,
|
|
||||||
) -> Result<(), GarageError> {
|
|
||||||
let metrics = Arc::new(WebMetrics::new());
|
let metrics = Arc::new(WebMetrics::new());
|
||||||
let web_server = Arc::new(WebServer {
|
Arc::new(WebServer {
|
||||||
garage,
|
garage,
|
||||||
metrics,
|
metrics,
|
||||||
root_domain,
|
root_domain,
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let tcp_service = make_service_fn(|conn: &AddrStream| {
|
pub async fn run(
|
||||||
let web_server = web_server.clone();
|
self: Arc<Self>,
|
||||||
|
bind_addr: UnixOrTCPSocketAddress,
|
||||||
|
shutdown_signal: impl Future<Output = ()>,
|
||||||
|
) -> Result<(), GarageError> {
|
||||||
|
info!("Web server listening on {}", bind_addr);
|
||||||
|
|
||||||
let client_addr = conn.remote_addr();
|
match bind_addr {
|
||||||
async move {
|
|
||||||
Ok::<_, Error>(service_fn(move |req: Request<Body>| {
|
|
||||||
let web_server = web_server.clone();
|
|
||||||
|
|
||||||
web_server.handle_request(req, client_addr.to_string())
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let unix_service = make_service_fn(|_: &UnixStream| {
|
|
||||||
let web_server = web_server.clone();
|
|
||||||
|
|
||||||
let path = addr.to_string();
|
|
||||||
async move {
|
|
||||||
Ok::<_, Error>(service_fn(move |req: Request<Body>| {
|
|
||||||
let web_server = web_server.clone();
|
|
||||||
|
|
||||||
web_server.handle_request(req, path.clone())
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
info!("Web server listening on {}", addr);
|
|
||||||
|
|
||||||
match addr {
|
|
||||||
UnixOrTCPSocketAddress::TCPSocket(addr) => {
|
UnixOrTCPSocketAddress::TCPSocket(addr) => {
|
||||||
Server::bind(&addr)
|
let listener = TcpListener::bind(addr).await?;
|
||||||
.serve(tcp_service)
|
|
||||||
.with_graceful_shutdown(shutdown_signal)
|
let handler =
|
||||||
.await?
|
move |stream, socketaddr| self.clone().handle_request(stream, socketaddr);
|
||||||
|
server_loop(listener, handler, shutdown_signal).await
|
||||||
}
|
}
|
||||||
UnixOrTCPSocketAddress::UnixSocket(ref path) => {
|
UnixOrTCPSocketAddress::UnixSocket(ref path) => {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
fs::remove_file(path)?
|
fs::remove_file(path)?
|
||||||
}
|
}
|
||||||
|
|
||||||
let bound = Server::bind_unix(path)?;
|
let listener = UnixListener::bind(path)?;
|
||||||
|
let listener = UnixListenerOn(listener, path.display().to_string());
|
||||||
|
|
||||||
fs::set_permissions(path, Permissions::from_mode(0o222))?;
|
fs::set_permissions(path, Permissions::from_mode(0o222))?;
|
||||||
|
|
||||||
bound
|
let handler =
|
||||||
.serve(unix_service)
|
move |stream, socketaddr| self.clone().handle_request(stream, socketaddr);
|
||||||
.with_graceful_shutdown(shutdown_signal)
|
server_loop(listener, handler, shutdown_signal).await
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_request(
|
async fn handle_request(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
req: Request<Body>,
|
req: Request<IncomingBody>,
|
||||||
addr: String,
|
addr: String,
|
||||||
) -> Result<Response<Body>, Infallible> {
|
) -> Result<Response<BoxBody<Error>>, http::Error> {
|
||||||
if let Ok(forwarded_for_ip_addr) =
|
if let Ok(forwarded_for_ip_addr) =
|
||||||
forwarded_headers::handle_forwarded_for_headers(req.headers())
|
forwarded_headers::handle_forwarded_for_headers(req.headers())
|
||||||
{
|
{
|
||||||
|
@ -187,7 +159,8 @@ impl WebServer {
|
||||||
match res {
|
match res {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
debug!("{} {} {}", req.method(), res.status(), req.uri());
|
debug!("{} {} {}", req.method(), res.status(), req.uri());
|
||||||
Ok(res)
|
Ok(res
|
||||||
|
.map(|body| BoxBody::new(http_body_util::BodyExt::map_err(body, Error::from))))
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
info!(
|
info!(
|
||||||
|
@ -220,7 +193,10 @@ impl WebServer {
|
||||||
Ok(exists)
|
Ok(exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_file(self: &Arc<Self>, req: &Request<Body>) -> Result<Response<Body>, Error> {
|
async fn serve_file(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
req: &Request<IncomingBody>,
|
||||||
|
) -> 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
|
||||||
.headers()
|
.headers()
|
||||||
|
@ -267,9 +243,11 @@ impl WebServer {
|
||||||
);
|
);
|
||||||
|
|
||||||
let ret_doc = match *req.method() {
|
let ret_doc = match *req.method() {
|
||||||
Method::OPTIONS => handle_options_for_bucket(req, &bucket),
|
Method::OPTIONS => handle_options_for_bucket(req, &bucket)
|
||||||
Method::HEAD => handle_head(self.garage.clone(), req, bucket_id, &key, None).await,
|
.map_err(ApiError::from)
|
||||||
Method::GET => handle_get(self.garage.clone(), req, bucket_id, &key, None).await,
|
.map(|res| res.map(|_empty_body: EmptyBody| empty_body())),
|
||||||
|
Method::HEAD => handle_head(self.garage.clone(), &req, bucket_id, &key, None).await,
|
||||||
|
Method::GET => handle_get(self.garage.clone(), &req, bucket_id, &key, None).await,
|
||||||
_ => Err(ApiError::bad_request("HTTP method not supported")),
|
_ => Err(ApiError::bad_request("HTTP method not supported")),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -281,7 +259,7 @@ impl WebServer {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::FOUND)
|
.status(StatusCode::FOUND)
|
||||||
.header("Location", url)
|
.header("Location", url)
|
||||||
.body(Body::empty())
|
.body(empty_body())
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
_ => ret_doc,
|
_ => ret_doc,
|
||||||
|
@ -310,7 +288,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(Body::empty())
|
.body(empty_body::<Infallible>())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
match handle_get(self.garage.clone(), &req2, bucket_id, &error_document, None).await
|
match handle_get(self.garage.clone(), &req2, bucket_id, &error_document, None).await
|
||||||
|
@ -358,7 +336,7 @@ impl WebServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error_to_res(e: Error) -> Response<Body> {
|
fn error_to_res(e: Error) -> Response<BoxBody<Error>> {
|
||||||
// If we are here, it is either that:
|
// If we are here, it is either that:
|
||||||
// - there was an error before trying to get the requested URL
|
// - there was an error before trying to get the requested URL
|
||||||
// from the bucket (e.g. bucket not found)
|
// from the bucket (e.g. bucket not found)
|
||||||
|
@ -366,7 +344,7 @@ fn error_to_res(e: Error) -> Response<Body> {
|
||||||
// was a HEAD request or we couldn't get the error document)
|
// was a HEAD request or we couldn't get the error document)
|
||||||
// We do NOT enter this code path when returning the bucket's
|
// We do NOT enter this code path when returning the bucket's
|
||||||
// error document (this is handled in serve_file)
|
// error document (this is handled in serve_file)
|
||||||
let body = Body::from(format!("{}\n", e));
|
let body = string_body(format!("{}\n", e));
|
||||||
let mut http_error = Response::new(body);
|
let mut http_error = Response::new(body);
|
||||||
*http_error.status_mut() = e.http_status_code();
|
*http_error.status_mut() = e.http_status_code();
|
||||||
e.add_headers(http_error.headers_mut());
|
e.add_headers(http_error.headers_mut());
|
||||||
|
|
Loading…
Reference in a new issue