aboutsummaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
authortrinity-1686a <trinity.pointard@gmail.com>2021-12-15 10:41:39 +0100
committerAlex <alex@adnab.me>2021-12-15 10:41:39 +0100
commit60d445992632a4962f709f9c9a19f698caf0e4af (patch)
tree7acf96d8cf8c8095e99aad46e55aaa23b80daf71 /src/api
parent3b3a1f275f18e4b6cbf1fb326ff9263fcf85460c (diff)
downloadgarage-60d445992632a4962f709f9c9a19f698caf0e4af.tar.gz
garage-60d445992632a4962f709f9c9a19f698caf0e4af.zip
BucketWebsite (#174)
fix #77 this does not store anything but a on/off switch for website, and does not implement GetBucketWebsite as it would require storing more. GetBucketWebsite should be pretty easy to implement once data is stored though. Co-authored-by: Trinity Pointard <trinity.pointard@gmail.com> Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/174 Co-authored-by: trinity-1686a <trinity.pointard@gmail.com> Co-committed-by: trinity-1686a <trinity.pointard@gmail.com>
Diffstat (limited to 'src/api')
-rw-r--r--src/api/api_server.rs5
-rw-r--r--src/api/lib.rs1
-rw-r--r--src/api/s3_website.rs308
-rw-r--r--src/api/s3_xml.rs8
4 files changed, 318 insertions, 4 deletions
diff --git a/src/api/api_server.rs b/src/api/api_server.rs
index 6d6e5b68..2de86233 100644
--- a/src/api/api_server.rs
+++ b/src/api/api_server.rs
@@ -22,6 +22,7 @@ use crate::s3_get::*;
use crate::s3_list::*;
use crate::s3_put::*;
use crate::s3_router::{Authorization, Endpoint};
+use crate::s3_website::*;
/// Run the S3 API server
pub async fn run_api_server(
@@ -254,6 +255,10 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
Endpoint::DeleteObjects { bucket } => {
handle_delete_objects(garage, &bucket, req, content_sha256).await
}
+ Endpoint::PutBucketWebsite { bucket } => {
+ handle_put_website(garage, bucket, req, content_sha256).await
+ }
+ Endpoint::DeleteBucketWebsite { bucket } => handle_delete_website(garage, bucket).await,
endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())),
}
}
diff --git a/src/api/lib.rs b/src/api/lib.rs
index 09a55d56..589ffe9f 100644
--- a/src/api/lib.rs
+++ b/src/api/lib.rs
@@ -20,4 +20,5 @@ pub mod s3_get;
mod s3_list;
mod s3_put;
mod s3_router;
+mod s3_website;
mod s3_xml;
diff --git a/src/api/s3_website.rs b/src/api/s3_website.rs
new file mode 100644
index 00000000..37c8b86c
--- /dev/null
+++ b/src/api/s3_website.rs
@@ -0,0 +1,308 @@
+use quick_xml::de::from_reader;
+use std::sync::Arc;
+
+use hyper::{Body, Request, Response, StatusCode};
+use serde::{Deserialize, Serialize};
+
+use crate::error::*;
+use crate::s3_xml::{xmlns_tag, IntValue, Value};
+use crate::signature::verify_signed_content;
+use garage_model::bucket_table::BucketState;
+use garage_model::garage::Garage;
+use garage_table::*;
+use garage_util::data::Hash;
+
+pub async fn handle_delete_website(
+ garage: Arc<Garage>,
+ bucket: String,
+) -> Result<Response<Body>, Error> {
+ let mut bucket = garage
+ .bucket_table
+ .get(&EmptyKey, &bucket)
+ .await?
+ .ok_or(Error::NotFound)?;
+
+ if let BucketState::Present(state) = bucket.state.get_mut() {
+ state.website.update(false);
+ garage.bucket_table.insert(&bucket).await?;
+ }
+
+ Ok(Response::builder()
+ .status(StatusCode::NO_CONTENT)
+ .body(Body::from(vec![]))
+ .unwrap())
+}
+
+pub async fn handle_put_website(
+ garage: Arc<Garage>,
+ bucket: String,
+ req: Request<Body>,
+ content_sha256: Option<Hash>,
+) -> Result<Response<Body>, Error> {
+ let body = hyper::body::to_bytes(req.into_body()).await?;
+ verify_signed_content(content_sha256, &body[..])?;
+
+ let mut bucket = garage
+ .bucket_table
+ .get(&EmptyKey, &bucket)
+ .await?
+ .ok_or(Error::NotFound)?;
+
+ let conf: WebsiteConfiguration = from_reader(&body as &[u8])?;
+ conf.validate()?;
+
+ if let BucketState::Present(state) = bucket.state.get_mut() {
+ state.website.update(true);
+ garage.bucket_table.insert(&bucket).await?;
+ }
+
+ Ok(Response::builder()
+ .status(StatusCode::OK)
+ .body(Body::from(vec![]))
+ .unwrap())
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct WebsiteConfiguration {
+ #[serde(serialize_with = "xmlns_tag", skip_deserializing)]
+ pub xmlns: (),
+ #[serde(rename = "ErrorDocument")]
+ pub error_document: Option<Key>,
+ #[serde(rename = "IndexDocument")]
+ pub index_document: Option<Suffix>,
+ #[serde(rename = "RedirectAllRequestsTo")]
+ pub redirect_all_requests_to: Option<Target>,
+ #[serde(rename = "RoutingRules")]
+ pub routing_rules: Option<Vec<RoutingRule>>,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct RoutingRule {
+ #[serde(rename = "RoutingRule")]
+ pub inner: RoutingRuleInner,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct RoutingRuleInner {
+ #[serde(rename = "Condition")]
+ pub condition: Option<Condition>,
+ #[serde(rename = "Redirect")]
+ pub redirect: Redirect,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Key {
+ #[serde(rename = "Key")]
+ pub key: Value,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Suffix {
+ #[serde(rename = "Suffix")]
+ pub suffix: Value,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Target {
+ #[serde(rename = "HostName")]
+ pub hostname: Value,
+ #[serde(rename = "Protocol")]
+ pub protocol: Option<Value>,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Condition {
+ #[serde(rename = "HttpErrorCodeReturnedEquals")]
+ pub http_error_code: Option<IntValue>,
+ #[serde(rename = "KeyPrefixEquals")]
+ pub prefix: Option<Value>,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Redirect {
+ #[serde(rename = "HostName")]
+ pub hostname: Option<Value>,
+ #[serde(rename = "Protocol")]
+ pub protocol: Option<Value>,
+ #[serde(rename = "HttpRedirectCode")]
+ pub http_redirect_code: Option<IntValue>,
+ #[serde(rename = "ReplaceKeyPrefixWith")]
+ pub replace_prefix: Option<Value>,
+ #[serde(rename = "ReplaceKeyWith")]
+ pub replace_full: Option<Value>,
+}
+
+impl WebsiteConfiguration {
+ pub fn validate(&self) -> Result<(), Error> {
+ if self.redirect_all_requests_to.is_some()
+ && (self.error_document.is_some()
+ || self.index_document.is_some()
+ || self.routing_rules.is_some())
+ {
+ return Err(Error::BadRequest(
+ "Bad XML: can't have RedirectAllRequestsTo and other fields".to_owned(),
+ ));
+ }
+ if let Some(ref ed) = self.error_document {
+ ed.validate()?;
+ }
+ if let Some(ref id) = self.index_document {
+ id.validate()?;
+ }
+ if let Some(ref rart) = self.redirect_all_requests_to {
+ rart.validate()?;
+ }
+ if let Some(ref rrs) = self.routing_rules {
+ for rr in rrs {
+ rr.inner.validate()?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl Key {
+ pub fn validate(&self) -> Result<(), Error> {
+ if self.key.0.is_empty() {
+ Err(Error::BadRequest(
+ "Bad XML: error document specified but empty".to_owned(),
+ ))
+ } else {
+ Ok(())
+ }
+ }
+}
+
+impl Suffix {
+ pub fn validate(&self) -> Result<(), Error> {
+ if self.suffix.0.is_empty() | self.suffix.0.contains('/') {
+ Err(Error::BadRequest(
+ "Bad XML: index document is empty or contains /".to_owned(),
+ ))
+ } else {
+ Ok(())
+ }
+ }
+}
+
+impl Target {
+ pub fn validate(&self) -> Result<(), Error> {
+ if let Some(ref protocol) = self.protocol {
+ if protocol.0 != "http" && protocol.0 != "https" {
+ return Err(Error::BadRequest("Bad XML: invalid protocol".to_owned()));
+ }
+ }
+ Ok(())
+ }
+}
+
+impl RoutingRuleInner {
+ pub fn validate(&self) -> Result<(), Error> {
+ let has_prefix = self
+ .condition
+ .as_ref()
+ .map(|c| c.prefix.as_ref())
+ .flatten()
+ .is_some();
+ self.redirect.validate(has_prefix)
+ }
+}
+
+impl Redirect {
+ pub fn validate(&self, has_prefix: bool) -> Result<(), Error> {
+ if self.replace_prefix.is_some() {
+ if self.replace_full.is_some() {
+ return Err(Error::BadRequest(
+ "Bad XML: both ReplaceKeyPrefixWith and ReplaceKeyWith are set".to_owned(),
+ ));
+ }
+ if !has_prefix {
+ return Err(Error::BadRequest(
+ "Bad XML: ReplaceKeyPrefixWith is set, but KeyPrefixEquals isn't".to_owned(),
+ ));
+ }
+ }
+ if let Some(ref protocol) = self.protocol {
+ if protocol.0 != "http" && protocol.0 != "https" {
+ return Err(Error::BadRequest("Bad XML: invalid protocol".to_owned()));
+ }
+ }
+ // TODO there are probably more invalide cases, but which ones?
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use quick_xml::de::from_str;
+
+ #[test]
+ fn test_deserialize() {
+ let message = r#"<?xml version="1.0" encoding="UTF-8"?>
+<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+ <ErrorDocument>
+ <Key>my-error-doc</Key>
+ </ErrorDocument>
+ <IndexDocument>
+ <Suffix>my-index</Suffix>
+ </IndexDocument>
+ <RedirectAllRequestsTo>
+ <HostName>garage.tld</HostName>
+ <Protocol>https</Protocol>
+ </RedirectAllRequestsTo>
+ <RoutingRules>
+ <RoutingRule>
+ <Condition>
+ <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
+ <KeyPrefixEquals>prefix1</KeyPrefixEquals>
+ </Condition>
+ <Redirect>
+ <HostName>gara.ge</HostName>
+ <Protocol>http</Protocol>
+ <HttpRedirectCode>303</HttpRedirectCode>
+ <ReplaceKeyPrefixWith>prefix2</ReplaceKeyPrefixWith>
+ <ReplaceKeyWith>fullkey</ReplaceKeyWith>
+ </Redirect>
+ </RoutingRule>
+ </RoutingRules>
+</WebsiteConfiguration>"#;
+ let conf: WebsiteConfiguration = from_str(message).unwrap();
+ let ref_value = WebsiteConfiguration {
+ xmlns: (),
+ error_document: Some(Key {
+ key: Value("my-error-doc".to_owned()),
+ }),
+ index_document: Some(Suffix {
+ suffix: Value("my-index".to_owned()),
+ }),
+ redirect_all_requests_to: Some(Target {
+ hostname: Value("garage.tld".to_owned()),
+ protocol: Some(Value("https".to_owned())),
+ }),
+ routing_rules: Some(vec![RoutingRule {
+ inner: RoutingRuleInner {
+ condition: Some(Condition {
+ http_error_code: Some(IntValue(404)),
+ prefix: Some(Value("prefix1".to_owned())),
+ }),
+ redirect: Redirect {
+ hostname: Some(Value("gara.ge".to_owned())),
+ protocol: Some(Value("http".to_owned())),
+ http_redirect_code: Some(IntValue(303)),
+ replace_prefix: Some(Value("prefix2".to_owned())),
+ replace_full: Some(Value("fullkey".to_owned())),
+ },
+ },
+ }]),
+ };
+ assert_eq! {
+ ref_value,
+ conf
+ }
+ // TODO verify result is ok
+ // TODO cycle back and verify if ok
+ }
+}
diff --git a/src/api/s3_xml.rs b/src/api/s3_xml.rs
index f0547961..9b5a0202 100644
--- a/src/api/s3_xml.rs
+++ b/src/api/s3_xml.rs
@@ -1,5 +1,5 @@
use quick_xml::se::to_string;
-use serde::{Serialize, Serializer};
+use serde::{Deserialize, Serialize, Serializer};
use crate::Error as ApiError;
@@ -9,14 +9,14 @@ pub fn to_xml_with_header<T: Serialize>(x: &T) -> Result<String, ApiError> {
Ok(xml)
}
-fn xmlns_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
+pub fn xmlns_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
s.serialize_str("http://s3.amazonaws.com/doc/2006-03-01/")
}
-#[derive(Debug, Serialize, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Value(#[serde(rename = "$value")] pub String);
-#[derive(Debug, Serialize, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct IntValue(#[serde(rename = "$value")] pub i64);
#[derive(Debug, Serialize, PartialEq)]