Implement CORS #183
12 changed files with 578 additions and 75 deletions
|
@ -9,7 +9,8 @@ Implemented:
|
||||||
- putting and getting objects in buckets
|
- putting and getting objects in buckets
|
||||||
- multipart uploads
|
- multipart uploads
|
||||||
- listing objects
|
- listing objects
|
||||||
- access control on a per-key-per-bucket basis
|
- access control on a per-access-key-per-bucket basis
|
||||||
|
- CORS headers on web endpoint
|
||||||
|
|
||||||
Not implemented:
|
Not implemented:
|
||||||
|
|
||||||
|
@ -31,9 +32,11 @@ All APIs that are not mentionned are not implemented and will return a 501 Not I
|
||||||
| CreateBucket | Implemented |
|
| CreateBucket | Implemented |
|
||||||
| CreateMultipartUpload | Implemented |
|
| CreateMultipartUpload | Implemented |
|
||||||
| DeleteBucket | Implemented |
|
| DeleteBucket | Implemented |
|
||||||
|
| DeleteBucketCors | Implemented |
|
||||||
| DeleteBucketWebsite | Implemented |
|
| DeleteBucketWebsite | Implemented |
|
||||||
| DeleteObject | Implemented |
|
| DeleteObject | Implemented |
|
||||||
| DeleteObjects | Implemented |
|
| DeleteObjects | Implemented |
|
||||||
|
| GetBucketCors | Implemented |
|
||||||
| GetBucketLocation | Implemented |
|
| GetBucketLocation | Implemented |
|
||||||
| GetBucketVersioning | Stub (see below) |
|
| GetBucketVersioning | Stub (see below) |
|
||||||
| GetBucketWebsite | Implemented |
|
| GetBucketWebsite | Implemented |
|
||||||
|
@ -46,6 +49,7 @@ All APIs that are not mentionned are not implemented and will return a 501 Not I
|
||||||
| ListMultipartUpload | Implemented |
|
| ListMultipartUpload | Implemented |
|
||||||
| ListParts | Implemented |
|
| ListParts | Implemented |
|
||||||
| PutObject | Implemented |
|
| PutObject | Implemented |
|
||||||
|
| PutBucketCors | Implemented |
|
||||||
| PutBucketWebsite | Partially implemented (see below)|
|
| PutBucketWebsite | Partially implemented (see below)|
|
||||||
| UploadPart | Implemented |
|
| UploadPart | Implemented |
|
||||||
| UploadPartCopy | Implemented |
|
| UploadPartCopy | Implemented |
|
||||||
|
|
|
@ -26,10 +26,10 @@ your motivations for doing so in the PR message.
|
||||||
| | UploadPart |
|
| | UploadPart |
|
||||||
| | [*ListMultipartUploads*](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/103) |
|
| | [*ListMultipartUploads*](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/103) |
|
||||||
| | [*ListParts*](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/103) |
|
| | [*ListParts*](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/103) |
|
||||||
| **A-tier** (will implement) | |
|
| **A-tier** | |
|
||||||
| | [*GetBucketCors*](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/138) |
|
| | GetBucketCors |
|
||||||
| | [*PutBucketCors*](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/138) |
|
| | PutBucketCors |
|
||||||
| | [*DeleteBucketCors*](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/138) |
|
| | DeleteBucketCors |
|
||||||
| | UploadPartCopy |
|
| | UploadPartCopy |
|
||||||
| | GetBucketWebsite |
|
| | GetBucketWebsite |
|
||||||
| | PutBucketWebsite |
|
| | PutBucketWebsite |
|
||||||
|
|
|
@ -5,7 +5,7 @@ use futures::future::Future;
|
||||||
use hyper::header;
|
use hyper::header;
|
||||||
use hyper::server::conn::AddrStream;
|
use hyper::server::conn::AddrStream;
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use hyper::{Body, Request, Response, Server};
|
use hyper::{Body, Method, Request, Response, Server};
|
||||||
|
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
use garage_util::error::Error as GarageError;
|
use garage_util::error::Error as GarageError;
|
||||||
|
@ -13,12 +13,15 @@ use garage_util::error::Error as GarageError;
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_model::key_table::Key;
|
use garage_model::key_table::Key;
|
||||||
|
|
||||||
|
use garage_table::util::*;
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::signature::payload::check_payload_signature;
|
use crate::signature::payload::check_payload_signature;
|
||||||
|
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
use crate::s3_bucket::*;
|
use crate::s3_bucket::*;
|
||||||
use crate::s3_copy::*;
|
use crate::s3_copy::*;
|
||||||
|
use crate::s3_cors::*;
|
||||||
use crate::s3_delete::*;
|
use crate::s3_delete::*;
|
||||||
use crate::s3_get::*;
|
use crate::s3_get::*;
|
||||||
use crate::s3_list::*;
|
use crate::s3_list::*;
|
||||||
|
@ -102,17 +105,17 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||||
|
|
||||||
let host = authority_to_host(authority)?;
|
let host = authority_to_host(authority)?;
|
||||||
|
|
||||||
let bucket = garage
|
let bucket_name = garage
|
||||||
.config
|
.config
|
||||||
.s3_api
|
.s3_api
|
||||||
.root_domain
|
.root_domain
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|root_domain| host_to_bucket(&host, root_domain));
|
.and_then(|root_domain| host_to_bucket(&host, root_domain));
|
||||||
|
|
||||||
let (endpoint, bucket) = Endpoint::from_request(&req, bucket.map(ToOwned::to_owned))?;
|
let (endpoint, bucket_name) = Endpoint::from_request(&req, bucket_name.map(ToOwned::to_owned))?;
|
||||||
debug!("Endpoint: {:?}", endpoint);
|
debug!("Endpoint: {:?}", endpoint);
|
||||||
|
|
||||||
let bucket_name = match bucket {
|
let bucket_name = match bucket_name {
|
||||||
None => return handle_request_without_bucket(garage, req, api_key, endpoint).await,
|
None => return handle_request_without_bucket(garage, req, api_key, endpoint).await,
|
||||||
Some(bucket) => bucket.to_string(),
|
Some(bucket) => bucket.to_string(),
|
||||||
};
|
};
|
||||||
|
@ -123,6 +126,12 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||||
}
|
}
|
||||||
|
|
||||||
let bucket_id = resolve_bucket(&garage, &bucket_name, &api_key).await?;
|
let bucket_id = resolve_bucket(&garage, &bucket_name, &api_key).await?;
|
||||||
|
let bucket = garage
|
||||||
|
.bucket_table
|
||||||
|
.get(&EmptyKey, &bucket_id)
|
||||||
|
.await?
|
||||||
|
.filter(|b| !b.state.is_deleted())
|
||||||
|
.ok_or(Error::NoSuchBucket)?;
|
||||||
|
|
||||||
let allowed = match endpoint.authorization_type() {
|
let allowed = match endpoint.authorization_type() {
|
||||||
Authorization::Read => api_key.allow_read(&bucket_id),
|
Authorization::Read => api_key.allow_read(&bucket_id),
|
||||||
|
@ -137,7 +146,17 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
match endpoint {
|
// Look up what CORS rule might apply to response.
|
||||||
|
// Requests for methods different than GET, HEAD or POST
|
||||||
|
// are always preflighted, i.e. the browser should make
|
||||||
|
// an OPTIONS call before to check it is allowed
|
||||||
|
let matching_cors_rule = match *req.method() {
|
||||||
|
Method::GET | Method::HEAD | Method::POST => find_matching_cors_rule(&bucket, &req)?,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = match endpoint {
|
||||||
|
Endpoint::Options => handle_options(garage, &req, bucket_id).await,
|
||||||
Endpoint::HeadObject { key, .. } => handle_head(garage, &req, bucket_id, &key).await,
|
Endpoint::HeadObject { key, .. } => handle_head(garage, &req, bucket_id, &key).await,
|
||||||
Endpoint::GetObject { key, .. } => handle_get(garage, &req, bucket_id, &key).await,
|
Endpoint::GetObject { key, .. } => handle_get(garage, &req, bucket_id, &key).await,
|
||||||
Endpoint::UploadPart {
|
Endpoint::UploadPart {
|
||||||
|
@ -320,8 +339,21 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||||
handle_put_website(garage, bucket_id, req, content_sha256).await
|
handle_put_website(garage, bucket_id, req, content_sha256).await
|
||||||
}
|
}
|
||||||
Endpoint::DeleteBucketWebsite {} => handle_delete_website(garage, bucket_id).await,
|
Endpoint::DeleteBucketWebsite {} => handle_delete_website(garage, bucket_id).await,
|
||||||
|
Endpoint::GetBucketCors {} => handle_get_cors(garage, bucket_id).await,
|
||||||
|
Endpoint::PutBucketCors {} => handle_put_cors(garage, bucket_id, req, content_sha256).await,
|
||||||
|
Endpoint::DeleteBucketCors {} => handle_delete_cors(garage, bucket_id).await,
|
||||||
endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())),
|
endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If request was a success and we have a CORS rule that applies to it,
|
||||||
|
// add the corresponding CORS headers to the response
|
||||||
|
let mut resp_ok = resp?;
|
||||||
|
if let Some(rule) = matching_cors_rule {
|
||||||
|
add_cors_headers(&mut resp_ok, rule)
|
||||||
|
.ok_or_internal_error("Invalid bucket CORS configuration")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(resp_ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_request_without_bucket(
|
async fn handle_request_without_bucket(
|
||||||
|
|
|
@ -15,6 +15,7 @@ mod signature;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
mod s3_bucket;
|
mod s3_bucket;
|
||||||
mod s3_copy;
|
mod s3_copy;
|
||||||
|
pub mod s3_cors;
|
||||||
mod s3_delete;
|
mod s3_delete;
|
||||||
pub mod s3_get;
|
pub mod s3_get;
|
||||||
mod s3_list;
|
mod s3_list;
|
||||||
|
|
409
src/api/s3_cors.rs
Normal file
409
src/api/s3_cors.rs
Normal file
|
@ -0,0 +1,409 @@
|
||||||
|
use quick_xml::de::from_reader;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use http::header::{
|
||||||
|
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||||
|
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD,
|
||||||
|
};
|
||||||
|
use hyper::{header::HeaderName, Body, Method, Request, Response, StatusCode};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::s3_xml::{to_xml_with_header, xmlns_tag, IntValue, Value};
|
||||||
|
use crate::signature::verify_signed_content;
|
||||||
|
|
||||||
|
use garage_model::bucket_table::{Bucket, CorsRule as GarageCorsRule};
|
||||||
|
use garage_model::garage::Garage;
|
||||||
|
use garage_table::*;
|
||||||
|
use garage_util::data::*;
|
||||||
|
|
||||||
|
pub async fn handle_get_cors(
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
bucket_id: Uuid,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let bucket = garage
|
||||||
|
.bucket_table
|
||||||
|
.get(&EmptyKey, &bucket_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::NoSuchBucket)?;
|
||||||
|
|
||||||
|
let param = bucket
|
||||||
|
.params()
|
||||||
|
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
||||||
|
|
||||||
|
if let Some(cors) = param.cors_config.get() {
|
||||||
|
let wc = CorsConfiguration {
|
||||||
|
xmlns: (),
|
||||||
|
cors_rules: cors
|
||||||
|
.iter()
|
||||||
|
.map(CorsRule::from_garage_cors_rule)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
};
|
||||||
|
let xml = to_xml_with_header(&wc)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(http::header::CONTENT_TYPE, "application/xml")
|
||||||
|
.body(Body::from(xml))?)
|
||||||
|
} else {
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::NO_CONTENT)
|
||||||
|
.body(Body::empty())?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_delete_cors(
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
bucket_id: Uuid,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let mut bucket = garage
|
||||||
|
.bucket_table
|
||||||
|
.get(&EmptyKey, &bucket_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::NoSuchBucket)?;
|
||||||
|
|
||||||
|
let param = bucket
|
||||||
|
.params_mut()
|
||||||
|
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
||||||
|
|
||||||
|
param.cors_config.update(None);
|
||||||
|
garage.bucket_table.insert(&bucket).await?;
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::NO_CONTENT)
|
||||||
|
.body(Body::empty())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_put_cors(
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
bucket_id: Uuid,
|
||||||
|
req: Request<Body>,
|
||||||
|
content_sha256: Option<Hash>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||||
|
|
||||||
|
if let Some(content_sha256) = content_sha256 {
|
||||||
|
verify_signed_content(content_sha256, &body[..])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bucket = garage
|
||||||
|
.bucket_table
|
||||||
|
.get(&EmptyKey, &bucket_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::NoSuchBucket)?;
|
||||||
|
|
||||||
|
let param = bucket
|
||||||
|
.params_mut()
|
||||||
|
.ok_or_internal_error("Bucket should not be deleted at this point")?;
|
||||||
|
|
||||||
|
let conf: CorsConfiguration = from_reader(&body as &[u8])?;
|
||||||
|
conf.validate()?;
|
||||||
|
|
||||||
|
param
|
||||||
|
.cors_config
|
||||||
|
.update(Some(conf.into_garage_cors_config()?));
|
||||||
|
garage.bucket_table.insert(&bucket).await?;
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::empty())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_options(
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
req: &Request<Body>,
|
||||||
|
bucket_id: Uuid,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let bucket = garage
|
||||||
|
.bucket_table
|
||||||
|
.get(&EmptyKey, &bucket_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::NoSuchBucket)?;
|
||||||
|
let origin = req
|
||||||
|
.headers()
|
||||||
|
.get("Origin")
|
||||||
|
.ok_or_bad_request("Missing Origin header")?
|
||||||
|
.to_str()?;
|
||||||
|
let request_method = req
|
||||||
|
.headers()
|
||||||
|
.get(ACCESS_CONTROL_REQUEST_METHOD)
|
||||||
|
.ok_or_bad_request("Missing Access-Control-Request-Method header")?
|
||||||
|
.to_str()?;
|
||||||
|
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
|
||||||
|
Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(),
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(cors_config) = bucket.params().unwrap().cors_config.get() {
|
||||||
|
let matching_rule = cors_config
|
||||||
|
.iter()
|
||||||
|
.find(|rule| cors_rule_matches(rule, origin, request_method, request_headers.iter()));
|
||||||
|
if let Some(rule) = matching_rule {
|
||||||
|
let mut resp = Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::empty())?;
|
||||||
|
add_cors_headers(&mut resp, rule).ok_or_internal_error("Invalid CORS configuration")?;
|
||||||
|
return Ok(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::Forbidden("This CORS request is not allowed.".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_matching_cors_rule<'a>(
|
||||||
|
bucket: &'a Bucket,
|
||||||
|
req: &Request<Body>,
|
||||||
|
) -> Result<Option<&'a GarageCorsRule>, Error> {
|
||||||
|
if let Some(cors_config) = bucket.params().unwrap().cors_config.get() {
|
||||||
|
if let Some(origin) = req.headers().get("Origin") {
|
||||||
|
let origin = origin.to_str()?;
|
||||||
|
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
|
||||||
|
Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(),
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
return Ok(cors_config.iter().find(|rule| {
|
||||||
|
cors_rule_matches(
|
||||||
|
rule,
|
||||||
|
origin,
|
||||||
|
&req.method().to_string(),
|
||||||
|
request_headers.iter(),
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cors_rule_matches<'a, HI, S>(
|
||||||
|
rule: &GarageCorsRule,
|
||||||
|
origin: &'a str,
|
||||||
|
method: &'a str,
|
||||||
|
mut request_headers: HI,
|
||||||
|
) -> bool
|
||||||
|
where
|
||||||
|
HI: Iterator<Item = S>,
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
|
rule.allow_origins.iter().any(|x| x == "*" || x == origin)
|
||||||
|
&& rule.allow_methods.iter().any(|x| x == "*" || x == method)
|
||||||
|
&& request_headers.all(|h| {
|
||||||
|
rule.allow_headers
|
||||||
|
.iter()
|
||||||
|
.any(|x| x == "*" || x == h.as_ref())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_cors_headers(
|
||||||
|
resp: &mut Response<Body>,
|
||||||
|
rule: &GarageCorsRule,
|
||||||
|
) -> Result<(), http::header::InvalidHeaderValue> {
|
||||||
|
let h = resp.headers_mut();
|
||||||
|
h.insert(
|
||||||
|
ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||||
|
rule.allow_origins.join(", ").parse()?,
|
||||||
|
);
|
||||||
|
h.insert(
|
||||||
|
ACCESS_CONTROL_ALLOW_METHODS,
|
||||||
|
rule.allow_methods.join(", ").parse()?,
|
||||||
|
);
|
||||||
|
h.insert(
|
||||||
|
ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
|
rule.allow_headers.join(", ").parse()?,
|
||||||
|
);
|
||||||
|
h.insert(
|
||||||
|
ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||||
|
rule.expose_headers.join(", ").parse()?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- SERIALIZATION AND DESERIALIZATION TO/FROM S3 XML ----
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[serde(rename = "CORSConfiguration")]
|
||||||
|
pub struct CorsConfiguration {
|
||||||
|
#[serde(serialize_with = "xmlns_tag", skip_deserializing)]
|
||||||
|
pub xmlns: (),
|
||||||
|
#[serde(rename = "CORSRule")]
|
||||||
|
pub cors_rules: Vec<CorsRule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct CorsRule {
|
||||||
|
#[serde(rename = "ID")]
|
||||||
|
pub id: Option<Value>,
|
||||||
|
#[serde(rename = "MaxAgeSeconds")]
|
||||||
|
pub max_age_seconds: Option<IntValue>,
|
||||||
|
#[serde(rename = "AllowedOrigin")]
|
||||||
|
pub allowed_origins: Vec<Value>,
|
||||||
|
#[serde(rename = "AllowedMethod")]
|
||||||
|
pub allowed_methods: Vec<Value>,
|
||||||
|
#[serde(rename = "AllowedHeader", default)]
|
||||||
|
pub allowed_headers: Vec<Value>,
|
||||||
|
#[serde(rename = "ExposeHeader", default)]
|
||||||
|
pub expose_headers: Vec<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct AllowedMethod {
|
||||||
|
#[serde(rename = "AllowedMethod")]
|
||||||
|
pub allowed_method: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct AllowedHeader {
|
||||||
|
#[serde(rename = "AllowedHeader")]
|
||||||
|
pub allowed_header: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct ExposeHeader {
|
||||||
|
#[serde(rename = "ExposeHeader")]
|
||||||
|
pub expose_header: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CorsConfiguration {
|
||||||
|
pub fn validate(&self) -> Result<(), Error> {
|
||||||
|
for r in self.cors_rules.iter() {
|
||||||
|
r.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_garage_cors_config(self) -> Result<Vec<GarageCorsRule>, Error> {
|
||||||
|
Ok(self
|
||||||
|
.cors_rules
|
||||||
|
.iter()
|
||||||
|
.map(CorsRule::to_garage_cors_rule)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CorsRule {
|
||||||
|
pub fn validate(&self) -> Result<(), Error> {
|
||||||
|
for method in self.allowed_methods.iter() {
|
||||||
|
method
|
||||||
|
.0
|
||||||
|
.parse::<Method>()
|
||||||
|
.ok_or_bad_request("Invalid CORSRule method")?;
|
||||||
|
}
|
||||||
|
for header in self
|
||||||
|
.allowed_headers
|
||||||
|
.iter()
|
||||||
|
.chain(self.expose_headers.iter())
|
||||||
|
{
|
||||||
|
header
|
||||||
|
.0
|
||||||
|
.parse::<HeaderName>()
|
||||||
|
.ok_or_bad_request("Invalid HTTP header name")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_garage_cors_rule(&self) -> GarageCorsRule {
|
||||||
|
let convert_vec =
|
||||||
|
|vval: &[Value]| vval.iter().map(|x| x.0.to_owned()).collect::<Vec<String>>();
|
||||||
|
GarageCorsRule {
|
||||||
|
id: self.id.as_ref().map(|x| x.0.to_owned()),
|
||||||
|
max_age_seconds: self.max_age_seconds.as_ref().map(|x| x.0 as u64),
|
||||||
|
allow_origins: convert_vec(&self.allowed_origins),
|
||||||
|
allow_methods: convert_vec(&self.allowed_methods),
|
||||||
|
allow_headers: convert_vec(&self.allowed_headers),
|
||||||
|
expose_headers: convert_vec(&self.expose_headers),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_garage_cors_rule(rule: &GarageCorsRule) -> Self {
|
||||||
|
let convert_vec = |vval: &[String]| {
|
||||||
|
vval.iter()
|
||||||
|
.map(|x| Value(x.clone()))
|
||||||
|
.collect::<Vec<Value>>()
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
id: rule.id.as_ref().map(|x| Value(x.clone())),
|
||||||
|
max_age_seconds: rule.max_age_seconds.map(|x| IntValue(x as i64)),
|
||||||
|
allowed_origins: convert_vec(&rule.allow_origins),
|
||||||
|
allowed_methods: convert_vec(&rule.allow_methods),
|
||||||
|
allowed_headers: convert_vec(&rule.allow_headers),
|
||||||
|
expose_headers: convert_vec(&rule.expose_headers),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use quick_xml::de::from_str;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize() -> Result<(), Error> {
|
||||||
|
let message = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||||
|
<CORSRule>
|
||||||
|
<AllowedOrigin>http://www.example.com</AllowedOrigin>
|
||||||
|
|
||||||
|
<AllowedMethod>PUT</AllowedMethod>
|
||||||
|
<AllowedMethod>POST</AllowedMethod>
|
||||||
|
<AllowedMethod>DELETE</AllowedMethod>
|
||||||
|
|
||||||
|
<AllowedHeader>*</AllowedHeader>
|
||||||
|
</CORSRule>
|
||||||
|
<CORSRule>
|
||||||
|
<AllowedOrigin>*</AllowedOrigin>
|
||||||
|
<AllowedMethod>GET</AllowedMethod>
|
||||||
|
</CORSRule>
|
||||||
|
<CORSRule>
|
||||||
|
<ID>qsdfjklm</ID>
|
||||||
|
<MaxAgeSeconds>12345</MaxAgeSeconds>
|
||||||
|
<AllowedOrigin>https://perdu.com</AllowedOrigin>
|
||||||
|
|
||||||
|
<AllowedMethod>GET</AllowedMethod>
|
||||||
|
<AllowedMethod>DELETE</AllowedMethod>
|
||||||
|
<AllowedHeader>*</AllowedHeader>
|
||||||
|
<ExposeHeader>*</ExposeHeader>
|
||||||
|
</CORSRule>
|
||||||
|
</CORSConfiguration>"#;
|
||||||
|
let conf: CorsConfiguration = from_str(message).unwrap();
|
||||||
|
let ref_value = CorsConfiguration {
|
||||||
|
xmlns: (),
|
||||||
|
cors_rules: vec![
|
||||||
|
CorsRule {
|
||||||
|
id: None,
|
||||||
|
max_age_seconds: None,
|
||||||
|
allowed_origins: vec!["http://www.example.com".into()],
|
||||||
|
allowed_methods: vec!["PUT".into(), "POST".into(), "DELETE".into()],
|
||||||
|
allowed_headers: vec!["*".into()],
|
||||||
|
expose_headers: vec![],
|
||||||
|
},
|
||||||
|
CorsRule {
|
||||||
|
id: None,
|
||||||
|
max_age_seconds: None,
|
||||||
|
allowed_origins: vec!["*".into()],
|
||||||
|
allowed_methods: vec!["GET".into()],
|
||||||
|
allowed_headers: vec![],
|
||||||
|
expose_headers: vec![],
|
||||||
|
},
|
||||||
|
CorsRule {
|
||||||
|
id: Some("qsdfjklm".into()),
|
||||||
|
max_age_seconds: Some(IntValue(12345)),
|
||||||
|
allowed_origins: vec!["https://perdu.com".into()],
|
||||||
|
allowed_methods: vec!["GET".into(), "DELETE".into()],
|
||||||
|
allowed_headers: vec!["*".into()],
|
||||||
|
expose_headers: vec!["*".into()],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert_eq! {
|
||||||
|
ref_value,
|
||||||
|
conf
|
||||||
|
};
|
||||||
|
|
||||||
|
let message2 = to_xml_with_header(&ref_value)?;
|
||||||
|
|
||||||
|
let cleanup = |c: &str| c.replace(char::is_whitespace, "");
|
||||||
|
assert_eq!(cleanup(message), cleanup(&message2));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -327,6 +327,7 @@ pub enum Endpoint {
|
||||||
part_number_marker: Option<u64>,
|
part_number_marker: Option<u64>,
|
||||||
upload_id: String,
|
upload_id: String,
|
||||||
},
|
},
|
||||||
|
Options,
|
||||||
PutBucketAccelerateConfiguration {
|
PutBucketAccelerateConfiguration {
|
||||||
},
|
},
|
||||||
PutBucketAcl {
|
PutBucketAcl {
|
||||||
|
@ -434,6 +435,10 @@ impl Endpoint {
|
||||||
.unwrap_or((path.to_owned(), ""))
|
.unwrap_or((path.to_owned(), ""))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if *req.method() == Method::OPTIONS {
|
||||||
|
return Ok((Self::Options, Some(bucket)));
|
||||||
|
}
|
||||||
|
|
||||||
let key = percent_encoding::percent_decode_str(key)
|
let key = percent_encoding::percent_decode_str(key)
|
||||||
.decode_utf8()?
|
.decode_utf8()?
|
||||||
.into_owned();
|
.into_owned();
|
||||||
|
@ -665,7 +670,6 @@ impl Endpoint {
|
||||||
GetBucketAccelerateConfiguration,
|
GetBucketAccelerateConfiguration,
|
||||||
GetBucketAcl,
|
GetBucketAcl,
|
||||||
GetBucketAnalyticsConfiguration,
|
GetBucketAnalyticsConfiguration,
|
||||||
GetBucketCors,
|
|
||||||
GetBucketEncryption,
|
GetBucketEncryption,
|
||||||
GetBucketIntelligentTieringConfiguration,
|
GetBucketIntelligentTieringConfiguration,
|
||||||
GetBucketInventoryConfiguration,
|
GetBucketInventoryConfiguration,
|
||||||
|
@ -711,6 +715,9 @@ impl Endpoint {
|
||||||
GetBucketWebsite,
|
GetBucketWebsite,
|
||||||
PutBucketWebsite,
|
PutBucketWebsite,
|
||||||
DeleteBucketWebsite,
|
DeleteBucketWebsite,
|
||||||
|
GetBucketCors,
|
||||||
|
PutBucketCors,
|
||||||
|
DeleteBucketCors,
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
if readonly {
|
if readonly {
|
||||||
|
@ -1027,7 +1034,7 @@ mod tests {
|
||||||
OWNER_DELETE "/" => DeleteBucket
|
OWNER_DELETE "/" => DeleteBucket
|
||||||
DELETE "/?analytics&id=list1" => DeleteBucketAnalyticsConfiguration
|
DELETE "/?analytics&id=list1" => DeleteBucketAnalyticsConfiguration
|
||||||
DELETE "/?analytics&id=Id" => DeleteBucketAnalyticsConfiguration
|
DELETE "/?analytics&id=Id" => DeleteBucketAnalyticsConfiguration
|
||||||
DELETE "/?cors" => DeleteBucketCors
|
OWNER_DELETE "/?cors" => DeleteBucketCors
|
||||||
DELETE "/?encryption" => DeleteBucketEncryption
|
DELETE "/?encryption" => DeleteBucketEncryption
|
||||||
DELETE "/?intelligent-tiering&id=Id" => DeleteBucketIntelligentTieringConfiguration
|
DELETE "/?intelligent-tiering&id=Id" => DeleteBucketIntelligentTieringConfiguration
|
||||||
DELETE "/?inventory&id=list1" => DeleteBucketInventoryConfiguration
|
DELETE "/?inventory&id=list1" => DeleteBucketInventoryConfiguration
|
||||||
|
@ -1050,7 +1057,7 @@ mod tests {
|
||||||
GET "/?accelerate" => GetBucketAccelerateConfiguration
|
GET "/?accelerate" => GetBucketAccelerateConfiguration
|
||||||
GET "/?acl" => GetBucketAcl
|
GET "/?acl" => GetBucketAcl
|
||||||
GET "/?analytics&id=Id" => GetBucketAnalyticsConfiguration
|
GET "/?analytics&id=Id" => GetBucketAnalyticsConfiguration
|
||||||
GET "/?cors" => GetBucketCors
|
OWNER_GET "/?cors" => GetBucketCors
|
||||||
GET "/?encryption" => GetBucketEncryption
|
GET "/?encryption" => GetBucketEncryption
|
||||||
GET "/?intelligent-tiering&id=Id" => GetBucketIntelligentTieringConfiguration
|
GET "/?intelligent-tiering&id=Id" => GetBucketIntelligentTieringConfiguration
|
||||||
GET "/?inventory&id=list1" => GetBucketInventoryConfiguration
|
GET "/?inventory&id=list1" => GetBucketInventoryConfiguration
|
||||||
|
@ -1126,7 +1133,7 @@ mod tests {
|
||||||
PUT "/?acl" => PutBucketAcl
|
PUT "/?acl" => PutBucketAcl
|
||||||
PUT "/?analytics&id=report1" => PutBucketAnalyticsConfiguration
|
PUT "/?analytics&id=report1" => PutBucketAnalyticsConfiguration
|
||||||
PUT "/?analytics&id=Id" => PutBucketAnalyticsConfiguration
|
PUT "/?analytics&id=Id" => PutBucketAnalyticsConfiguration
|
||||||
PUT "/?cors" => PutBucketCors
|
OWNER_PUT "/?cors" => PutBucketCors
|
||||||
PUT "/?encryption" => PutBucketEncryption
|
PUT "/?encryption" => PutBucketEncryption
|
||||||
PUT "/?intelligent-tiering&id=Id" => PutBucketIntelligentTieringConfiguration
|
PUT "/?intelligent-tiering&id=Id" => PutBucketIntelligentTieringConfiguration
|
||||||
PUT "/?inventory&id=report1" => PutBucketInventoryConfiguration
|
PUT "/?inventory&id=report1" => PutBucketInventoryConfiguration
|
||||||
|
|
|
@ -5,7 +5,7 @@ use hyper::{Body, Request, Response, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::s3_xml::{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;
|
||||||
|
|
||||||
use garage_model::bucket_table::*;
|
use garage_model::bucket_table::*;
|
||||||
|
@ -39,7 +39,7 @@ pub async fn handle_get_website(
|
||||||
redirect_all_requests_to: None,
|
redirect_all_requests_to: None,
|
||||||
routing_rules: None,
|
routing_rules: None,
|
||||||
};
|
};
|
||||||
let xml = quick_xml::se::to_string(&wc)?;
|
let xml = to_xml_with_header(&wc)?;
|
||||||
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")
|
||||||
|
@ -306,7 +306,7 @@ mod tests {
|
||||||
use quick_xml::de::from_str;
|
use quick_xml::de::from_str;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialize() {
|
fn test_deserialize() -> Result<(), Error> {
|
||||||
let message = r#"<?xml version="1.0" encoding="UTF-8"?>
|
let message = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||||
<ErrorDocument>
|
<ErrorDocument>
|
||||||
|
@ -368,7 +368,12 @@ mod tests {
|
||||||
ref_value,
|
ref_value,
|
||||||
conf
|
conf
|
||||||
}
|
}
|
||||||
// TODO verify result is ok
|
|
||||||
// TODO cycle back and verify if ok
|
let message2 = to_xml_with_header(&ref_value)?;
|
||||||
|
|
||||||
|
let cleanup = |c: &str| c.replace(char::is_whitespace, "");
|
||||||
|
assert_eq!(cleanup(message), cleanup(&message2));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,12 @@ pub fn xmlns_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Value(#[serde(rename = "$value")] pub String);
|
pub struct Value(#[serde(rename = "$value")] pub String);
|
||||||
|
|
||||||
|
impl From<&str> for Value {
|
||||||
|
fn from(s: &str) -> Value {
|
||||||
|
Value(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct IntValue(#[serde(rename = "$value")] pub i64);
|
pub struct IntValue(#[serde(rename = "$value")] pub i64);
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,7 @@ pub struct BucketParams {
|
||||||
pub creation_date: u64,
|
pub creation_date: u64,
|
||||||
/// Map of key with access to the bucket, and what kind of access they give
|
/// Map of key with access to the bucket, and what kind of access they give
|
||||||
pub authorized_keys: crdt::Map<String, BucketKeyPerm>,
|
pub authorized_keys: crdt::Map<String, BucketKeyPerm>,
|
||||||
/// Whether this bucket is allowed for website access
|
|
||||||
/// (under all of its global alias names),
|
|
||||||
/// and if so, the website configuration XML document
|
|
||||||
pub website_config: crdt::Lww<Option<WebsiteConfig>>,
|
|
||||||
/// Map of aliases that are or have been given to this bucket
|
/// Map of aliases that are or have been given to this bucket
|
||||||
/// in the global namespace
|
/// in the global namespace
|
||||||
/// (not authoritative: this is just used as an indication to
|
/// (not authoritative: this is just used as an indication to
|
||||||
|
@ -40,6 +37,13 @@ pub struct BucketParams {
|
||||||
/// in namespaces local to keys
|
/// in namespaces local to keys
|
||||||
/// key = (access key id, alias name)
|
/// key = (access key id, alias name)
|
||||||
pub local_aliases: crdt::LwwMap<(String, String), bool>,
|
pub local_aliases: crdt::LwwMap<(String, String), bool>,
|
||||||
|
|
||||||
|
/// Whether this bucket is allowed for website access
|
||||||
|
/// (under all of its global alias names),
|
||||||
|
/// and if so, the website configuration XML document
|
||||||
|
pub website_config: crdt::Lww<Option<WebsiteConfig>>,
|
||||||
|
/// CORS rules
|
||||||
|
pub cors_config: crdt::Lww<Option<Vec<CorsRule>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
@ -48,15 +52,26 @@ pub struct WebsiteConfig {
|
||||||
pub error_document: Option<String>,
|
pub error_document: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CorsRule {
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub max_age_seconds: Option<u64>,
|
||||||
|
pub allow_origins: Vec<String>,
|
||||||
|
pub allow_methods: Vec<String>,
|
||||||
|
pub allow_headers: Vec<String>,
|
||||||
|
pub expose_headers: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl BucketParams {
|
impl BucketParams {
|
||||||
/// Create an empty BucketParams with no authorized keys and no website accesss
|
/// Create an empty BucketParams with no authorized keys and no website accesss
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
BucketParams {
|
BucketParams {
|
||||||
creation_date: now_msec(),
|
creation_date: now_msec(),
|
||||||
authorized_keys: crdt::Map::new(),
|
authorized_keys: crdt::Map::new(),
|
||||||
website_config: crdt::Lww::new(None),
|
|
||||||
aliases: crdt::LwwMap::new(),
|
aliases: crdt::LwwMap::new(),
|
||||||
local_aliases: crdt::LwwMap::new(),
|
local_aliases: crdt::LwwMap::new(),
|
||||||
|
website_config: crdt::Lww::new(None),
|
||||||
|
cors_config: crdt::Lww::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,9 +80,12 @@ impl Crdt for BucketParams {
|
||||||
fn merge(&mut self, o: &Self) {
|
fn merge(&mut self, o: &Self) {
|
||||||
self.creation_date = std::cmp::min(self.creation_date, o.creation_date);
|
self.creation_date = std::cmp::min(self.creation_date, o.creation_date);
|
||||||
self.authorized_keys.merge(&o.authorized_keys);
|
self.authorized_keys.merge(&o.authorized_keys);
|
||||||
self.website_config.merge(&o.website_config);
|
|
||||||
self.aliases.merge(&o.aliases);
|
self.aliases.merge(&o.aliases);
|
||||||
self.local_aliases.merge(&o.local_aliases);
|
self.local_aliases.merge(&o.local_aliases);
|
||||||
|
|
||||||
|
self.website_config.merge(&o.website_config);
|
||||||
|
self.cors_config.merge(&o.cors_config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,9 +69,10 @@ impl Migrate {
|
||||||
state: Deletable::Present(BucketParams {
|
state: Deletable::Present(BucketParams {
|
||||||
creation_date: now_msec(),
|
creation_date: now_msec(),
|
||||||
authorized_keys: Map::new(),
|
authorized_keys: Map::new(),
|
||||||
website_config: Lww::new(website),
|
|
||||||
aliases: LwwMap::new(),
|
aliases: LwwMap::new(),
|
||||||
local_aliases: LwwMap::new(),
|
local_aliases: LwwMap::new(),
|
||||||
|
website_config: Lww::new(website),
|
||||||
|
cors_config: Lww::new(None),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -125,3 +125,15 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Default for Lww<T>
|
||||||
|
where
|
||||||
|
T: Default,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ts: 0,
|
||||||
|
v: T::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,8 +11,9 @@ use hyper::{
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
|
|
||||||
use garage_api::error::{Error as ApiError, OkOrBadRequest};
|
use garage_api::error::{Error as ApiError, OkOrBadRequest, OkOrInternalError};
|
||||||
use garage_api::helpers::{authority_to_host, host_to_bucket};
|
use garage_api::helpers::{authority_to_host, host_to_bucket};
|
||||||
|
use garage_api::s3_cors::{add_cors_headers, find_matching_cors_rule};
|
||||||
use garage_api::s3_get::{handle_get, handle_head};
|
use garage_api::s3_get::{handle_get, handle_head};
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
|
@ -138,17 +139,17 @@ async fn serve_file(garage: Arc<Garage>, req: &Request<Body>) -> Result<Response
|
||||||
}
|
}
|
||||||
.map_err(Error::from);
|
.map_err(Error::from);
|
||||||
|
|
||||||
if let Err(error) = ret_doc {
|
match ret_doc {
|
||||||
|
Err(error) => {
|
||||||
|
// For a HEAD method, and for non-4xx errors,
|
||||||
|
// we don't return the error document as content,
|
||||||
|
// we return above and just return the error message
|
||||||
|
// by relying on err_to_res that is called when we return an Err.
|
||||||
if *req.method() == Method::HEAD || !error.http_status_code().is_client_error() {
|
if *req.method() == Method::HEAD || !error.http_status_code().is_client_error() {
|
||||||
// Do not return the error document in the following cases:
|
|
||||||
// - the error is not a 4xx error code
|
|
||||||
// - the request is a HEAD method
|
|
||||||
// In this case we just return the error code and the error message in the body,
|
|
||||||
// by relying on err_to_res that is called above when we return an Err.
|
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same if no error document is set: just return the error directly
|
// If no error document is set: just return the error directly
|
||||||
let error_document = match &website_config.error_document {
|
let error_document = match &website_config.error_document {
|
||||||
Some(ed) => ed.trim_start_matches('/').to_owned(),
|
Some(ed) => ed.trim_start_matches('/').to_owned(),
|
||||||
None => return Err(error),
|
None => return Err(error),
|
||||||
|
@ -193,8 +194,15 @@ async fn serve_file(garage: Arc<Garage>, req: &Request<Body>) -> Result<Response
|
||||||
Err(error)
|
Err(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
ret_doc
|
Ok(mut resp) => {
|
||||||
|
// Maybe add CORS headers
|
||||||
|
if let Some(rule) = find_matching_cors_rule(&bucket, req)? {
|
||||||
|
add_cors_headers(&mut resp, rule)
|
||||||
|
.ok_or_internal_error("Invalid bucket CORS configuration")?;
|
||||||
|
}
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue