Merge pull request 'Support website publishing' (#7) from feature/website into master
Reviewed-on: #7
This commit is contained in:
commit
97494b4a19
17 changed files with 480 additions and 14 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -425,6 +425,7 @@ dependencies = [
|
|||
"garage_rpc 0.1.0",
|
||||
"garage_table 0.1.1",
|
||||
"garage_util 0.1.0",
|
||||
"garage_web",
|
||||
"hex",
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
|
@ -666,6 +667,24 @@ dependencies = [
|
|||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garage_web"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"err-derive",
|
||||
"futures",
|
||||
"garage_api",
|
||||
"garage_model 0.1.1",
|
||||
"garage_table 0.1.1",
|
||||
"garage_util 0.1.0",
|
||||
"http",
|
||||
"hyper",
|
||||
"idna",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"roxmltree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.6.21"
|
||||
|
|
|
@ -5,6 +5,7 @@ members = [
|
|||
"src/table",
|
||||
"src/model",
|
||||
"src/api",
|
||||
"src/web",
|
||||
"src/garage",
|
||||
]
|
||||
|
||||
|
|
|
@ -92,7 +92,9 @@ api_bind_addr = "[::1]:3900" # the S3 API port, HTTP without TLS. Add a reverse
|
|||
s3_region = "garage" # set this to anything. S3 API calls will fail if they are not made against the region set here.
|
||||
|
||||
[s3_web]
|
||||
web_bind_addr = "[::1]:3902"
|
||||
bind_addr = "[::1]:3902"
|
||||
root_domain = ".garage.tld"
|
||||
index = "index.html"
|
||||
```
|
||||
|
||||
Build Garage using `cargo build --release`.
|
||||
|
|
|
@ -17,4 +17,6 @@ api_bind_addr = "[::1]:3900" # the S3 API port, HTTP without TLS. Add a reverse
|
|||
s3_region = "garage" # set this to anything. S3 API calls will fail if they are not made against the region set here.
|
||||
|
||||
[s3_web]
|
||||
web_bind_addr = "[::1]:3902"
|
||||
bind_addr = "[::1]:3902"
|
||||
root_domain = ".garage.tld"
|
||||
index = "index.html"
|
||||
|
|
|
@ -41,6 +41,8 @@ s3_region = "garage" # set this to anything. S3 API calls will fail if they a
|
|||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:$((3920+$count))"
|
||||
root_domain = ".garage.tld"
|
||||
index = "index.html"
|
||||
EOF
|
||||
|
||||
echo -en "$LABEL configuration written to $CONF_PATH\n"
|
||||
|
|
|
@ -6,6 +6,7 @@ shopt -s expand_aliases
|
|||
SCRIPT_FOLDER="`dirname \"$0\"`"
|
||||
REPO_FOLDER="${SCRIPT_FOLDER}/../"
|
||||
|
||||
echo "setup"
|
||||
cargo build
|
||||
${SCRIPT_FOLDER}/dev-clean.sh
|
||||
${SCRIPT_FOLDER}/dev-cluster.sh > /tmp/garage.log 2>&1 &
|
||||
|
@ -22,6 +23,7 @@ dd if=/dev/urandom of=/tmp/garage.1.rnd bs=1k count=2 # < INLINE_THRESHOLD = 307
|
|||
dd if=/dev/urandom of=/tmp/garage.2.rnd bs=1M count=5
|
||||
dd if=/dev/urandom of=/tmp/garage.3.rnd bs=1M count=10
|
||||
|
||||
echo "s3 api testing..."
|
||||
for idx in $(seq 1 3); do
|
||||
# AWS sends
|
||||
awsgrg cp /tmp/garage.$idx.rnd s3://eprouvette/garage.$idx.aws
|
||||
|
@ -55,6 +57,18 @@ for idx in $(seq 1 3); do
|
|||
done
|
||||
rm /tmp/garage.{1,2,3}.rnd
|
||||
|
||||
echo "website testing"
|
||||
echo "<h1>hello world</h1>" > /tmp/garage-index.html
|
||||
awsgrg cp /tmp/garage-index.html s3://eprouvette/index.html
|
||||
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ]
|
||||
garage bucket website --allow eprouvette
|
||||
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 200 ]
|
||||
garage bucket website --deny eprouvette
|
||||
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ]
|
||||
awsgrg rm s3://eprouvette/index.html
|
||||
rm /tmp/garage-index.html
|
||||
|
||||
echo "teardown"
|
||||
garage bucket deny --read --write eprouvette --key $AWS_ACCESS_KEY_ID
|
||||
garage bucket delete --yes eprouvette
|
||||
garage key delete --yes $AWS_ACCESS_KEY_ID
|
||||
|
|
|
@ -19,6 +19,7 @@ garage_rpc = { version = "0.1", path = "../rpc" }
|
|||
garage_table = { version = "0.1.1", path = "../table" }
|
||||
garage_model = { version = "0.1.1", path = "../model" }
|
||||
garage_api = { version = "0.1.1", path = "../api" }
|
||||
garage_web = { version = "0.1", path = "../web" }
|
||||
|
||||
bytes = "0.4"
|
||||
rand = "0.7"
|
||||
|
|
|
@ -89,7 +89,7 @@ impl AdminRpcHandler {
|
|||
}
|
||||
bucket
|
||||
.state
|
||||
.update(BucketState::Present(crdt::LWWMap::new()));
|
||||
.update(BucketState::Present(BucketParams::new()));
|
||||
bucket
|
||||
}
|
||||
None => Bucket::new(query.name.clone()),
|
||||
|
@ -155,6 +155,29 @@ impl AdminRpcHandler {
|
|||
&query.key_id, &query.bucket, allow_read, allow_write
|
||||
)))
|
||||
}
|
||||
BucketOperation::Website(query) => {
|
||||
let mut bucket = self.get_existing_bucket(&query.bucket).await?;
|
||||
|
||||
if !(query.allow ^ query.deny) {
|
||||
return Err(Error::Message(format!(
|
||||
"You must specify exactly one flag, either --allow or --deny"
|
||||
)));
|
||||
}
|
||||
|
||||
if let BucketState::Present(state) = bucket.state.get_mut() {
|
||||
state.website.update(query.allow);
|
||||
self.garage.bucket_table.insert(&bucket).await?;
|
||||
let msg = if query.allow {
|
||||
format!("Website access allowed for {}", &query.bucket)
|
||||
} else {
|
||||
format!("Website access denied for {}", &query.bucket)
|
||||
};
|
||||
|
||||
Ok(AdminRPC::Ok(msg.to_string()))
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,6 +260,7 @@ impl AdminRpcHandler {
|
|||
.unwrap_or(Err(Error::BadRPC(format!("Key {} does not exist", id))))
|
||||
}
|
||||
|
||||
/// Update **bucket table** to inform of the new linked key
|
||||
async fn update_bucket_key(
|
||||
&self,
|
||||
mut bucket: Bucket,
|
||||
|
@ -244,7 +268,8 @@ impl AdminRpcHandler {
|
|||
allow_read: bool,
|
||||
allow_write: bool,
|
||||
) -> Result<(), Error> {
|
||||
if let BucketState::Present(ak) = bucket.state.get_mut() {
|
||||
if let BucketState::Present(params) = bucket.state.get_mut() {
|
||||
let ak = &mut params.authorized_keys;
|
||||
let old_ak = ak.take_and_clear();
|
||||
ak.merge(&old_ak.update_mutator(
|
||||
key_id.to_string(),
|
||||
|
@ -262,6 +287,7 @@ impl AdminRpcHandler {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Update **key table** to inform of the new linked bucket
|
||||
async fn update_key_bucket(
|
||||
&self,
|
||||
mut key: Key,
|
||||
|
|
|
@ -141,6 +141,24 @@ pub enum BucketOperation {
|
|||
/// Allow key to read or write to bucket
|
||||
#[structopt(name = "deny")]
|
||||
Deny(PermBucketOpt),
|
||||
|
||||
/// Expose as website or not
|
||||
#[structopt(name = "website")]
|
||||
Website(WebsiteOpt),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
||||
pub struct WebsiteOpt {
|
||||
/// Create
|
||||
#[structopt(long = "allow")]
|
||||
pub allow: bool,
|
||||
|
||||
/// Delete
|
||||
#[structopt(long = "deny")]
|
||||
pub deny: bool,
|
||||
|
||||
/// Bucket name
|
||||
pub bucket: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
||||
|
|
|
@ -11,6 +11,7 @@ use garage_util::error::Error;
|
|||
use garage_api::api_server;
|
||||
use garage_model::garage::Garage;
|
||||
use garage_rpc::rpc_server::RpcServer;
|
||||
use garage_web::web_server;
|
||||
|
||||
use crate::admin_rpc::*;
|
||||
|
||||
|
@ -56,6 +57,7 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
|
|||
info!("Initializing RPC and API servers...");
|
||||
let run_rpc_server = Arc::new(rpc_server).run(wait_from(watch_cancel.clone()));
|
||||
let api_server = api_server::run_api_server(garage.clone(), wait_from(watch_cancel.clone()));
|
||||
let web_server = web_server::run_web_server(garage.clone(), wait_from(watch_cancel.clone()));
|
||||
|
||||
futures::try_join!(
|
||||
garage
|
||||
|
@ -78,6 +80,10 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
|
|||
info!("API server exited");
|
||||
rv
|
||||
}),
|
||||
web_server.map(|rv| {
|
||||
info!("Web server exited");
|
||||
rv
|
||||
}),
|
||||
background.run().map(|rv| {
|
||||
info!("Background runner exited");
|
||||
Ok(rv)
|
||||
|
|
|
@ -13,6 +13,11 @@ use crate::key_table::PermissionSet;
|
|||
// We use them to perform migrations.
|
||||
use model010::bucket_table as prev;
|
||||
|
||||
/// A bucket is a collection of objects
|
||||
///
|
||||
/// Its parameters are not directly accessible as:
|
||||
/// - It must be possible to merge paramaters, hence the use of a LWW CRDT.
|
||||
/// - A bucket has 2 states, Present or Deleted and parameters make sense only if present.
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Bucket {
|
||||
// Primary key
|
||||
|
@ -24,27 +29,49 @@ pub struct Bucket {
|
|||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum BucketState {
|
||||
Deleted,
|
||||
Present(crdt::LWWMap<String, PermissionSet>),
|
||||
Present(BucketParams),
|
||||
}
|
||||
|
||||
impl CRDT for BucketState {
|
||||
fn merge(&mut self, o: &Self) {
|
||||
match o {
|
||||
BucketState::Deleted => *self = BucketState::Deleted,
|
||||
BucketState::Present(other_ak) => {
|
||||
if let BucketState::Present(ak) = self {
|
||||
ak.merge(other_ak);
|
||||
BucketState::Present(other_params) => {
|
||||
if let BucketState::Present(params) = self {
|
||||
params.merge(other_params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BucketParams {
|
||||
pub authorized_keys: crdt::LWWMap<String, PermissionSet>,
|
||||
pub website: crdt::LWW<bool>,
|
||||
}
|
||||
|
||||
impl CRDT for BucketParams {
|
||||
fn merge(&mut self, o: &Self) {
|
||||
self.authorized_keys.merge(&o.authorized_keys);
|
||||
self.website.merge(&o.website);
|
||||
}
|
||||
}
|
||||
|
||||
impl BucketParams {
|
||||
pub fn new() -> Self {
|
||||
BucketParams {
|
||||
authorized_keys: crdt::LWWMap::new(),
|
||||
website: crdt::LWW::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Bucket {
|
||||
pub fn new(name: String) -> Self {
|
||||
Bucket {
|
||||
name,
|
||||
state: crdt::LWW::new(BucketState::Present(crdt::LWWMap::new())),
|
||||
state: crdt::LWW::new(BucketState::Present(BucketParams::new())),
|
||||
}
|
||||
}
|
||||
pub fn is_deleted(&self) -> bool {
|
||||
|
@ -53,7 +80,7 @@ impl Bucket {
|
|||
pub fn authorized_keys(&self) -> &[(String, u64, PermissionSet)] {
|
||||
match self.state.get() {
|
||||
BucketState::Deleted => &[],
|
||||
BucketState::Present(ak) => ak.items(),
|
||||
BucketState::Present(state) => state.authorized_keys.items(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,9 +137,15 @@ impl TableSchema for BucketTable {
|
|||
},
|
||||
));
|
||||
}
|
||||
|
||||
let params = BucketParams {
|
||||
authorized_keys: keys,
|
||||
website: crdt::LWW::new(false),
|
||||
};
|
||||
|
||||
Some(Bucket {
|
||||
name: old.name,
|
||||
state: crdt::LWW::migrate_from_raw(old.timestamp, BucketState::Present(keys)),
|
||||
state: crdt::LWW::migrate_from_raw(old.timestamp, BucketState::Present(params)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -239,7 +239,7 @@ where
|
|||
///
|
||||
/// Typically, to update the value associated to a key in the map, you would do the following:
|
||||
///
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// let my_update = my_crdt.update_mutator(key_to_modify, new_value);
|
||||
/// my_crdt.merge(&my_update);
|
||||
/// ```
|
||||
|
@ -261,7 +261,7 @@ where
|
|||
/// empty map. This is very usefull to produce in-place a new map that contains only a delta
|
||||
/// that modifies a certain value:
|
||||
///
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// let mut a = get_my_crdt_value();
|
||||
/// let old_a = a.take_and_clear();
|
||||
/// a.merge(&old_a.update_mutator(key_to_modify, new_value));
|
||||
|
@ -273,7 +273,7 @@ where
|
|||
/// but in the case where the map is a field in a struct for instance (as is always the case),
|
||||
/// this becomes very handy:
|
||||
///
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// let mut a = get_my_crdt_value();
|
||||
/// let old_a_map = a.map_field.take_and_clear();
|
||||
/// a.map_field.merge(&old_a_map.update_mutator(key_to_modify, new_value));
|
||||
|
|
|
@ -35,6 +35,8 @@ pub struct Config {
|
|||
pub rpc_tls: Option<TlsConfig>,
|
||||
|
||||
pub s3_api: ApiConfig,
|
||||
|
||||
pub s3_web: WebConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
|
@ -50,6 +52,13 @@ pub struct ApiConfig {
|
|||
pub s3_region: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct WebConfig {
|
||||
pub bind_addr: SocketAddr,
|
||||
pub root_domain: String,
|
||||
pub index: String,
|
||||
}
|
||||
|
||||
fn default_max_concurrent_rpc_requests() -> usize {
|
||||
12
|
||||
}
|
||||
|
|
28
src/web/Cargo.toml
Normal file
28
src/web/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "garage_web"
|
||||
version = "0.1.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0"
|
||||
description = "S3-like website endpoint crate for the Garage object store"
|
||||
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
garage_util = { version = "0.1", path = "../util" }
|
||||
garage_table = { version = "0.1.1", path = "../table" }
|
||||
garage_model = { version = "0.1.1", path = "../model" }
|
||||
garage_api = { version = "0.1.1", path = "../api" }
|
||||
|
||||
err-derive = "0.2.3"
|
||||
log = "0.4"
|
||||
futures = "0.3"
|
||||
http = "0.2"
|
||||
hyper = "0.13"
|
||||
percent-encoding = "2.1.0"
|
||||
roxmltree = "0.11"
|
||||
idna = "0.2"
|
39
src/web/error.rs
Normal file
39
src/web/error.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use err_derive::Error;
|
||||
use hyper::StatusCode;
|
||||
|
||||
use garage_util::error::Error as GarageError;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(display = "API error: {}", _0)]
|
||||
ApiError(#[error(source)] garage_api::error::Error),
|
||||
|
||||
// Category: internal error
|
||||
#[error(display = "Internal error: {}", _0)]
|
||||
InternalError(#[error(source)] GarageError),
|
||||
|
||||
#[error(display = "Not found")]
|
||||
NotFound,
|
||||
|
||||
// Category: bad request
|
||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
||||
InvalidUTF8(#[error(source)] std::str::Utf8Error),
|
||||
|
||||
#[error(display = "Invalid header value: {}", _0)]
|
||||
InvalidHeader(#[error(source)] hyper::header::ToStrError),
|
||||
|
||||
#[error(display = "Bad request: {}", _0)]
|
||||
BadRequest(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn http_status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::NotFound => StatusCode::NOT_FOUND,
|
||||
Error::ApiError(e) => e.http_status_code(),
|
||||
Error::InternalError(GarageError::RPC(_)) => StatusCode::SERVICE_UNAVAILABLE,
|
||||
Error::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
}
|
6
src/web/lib.rs
Normal file
6
src/web/lib.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub mod web_server;
|
260
src/web/web_server.rs
Normal file
260
src/web/web_server.rs
Normal file
|
@ -0,0 +1,260 @@
|
|||
use std::{borrow::Cow, convert::Infallible, net::SocketAddr, sync::Arc};
|
||||
|
||||
use futures::future::Future;
|
||||
|
||||
use hyper::{
|
||||
header::HOST,
|
||||
server::conn::AddrStream,
|
||||
service::{make_service_fn, service_fn},
|
||||
Body, Method, Request, Response, Server,
|
||||
};
|
||||
|
||||
use idna::domain_to_unicode;
|
||||
|
||||
use crate::error::*;
|
||||
use garage_api::s3_get::{handle_get, handle_head};
|
||||
use garage_model::bucket_table::*;
|
||||
use garage_model::garage::Garage;
|
||||
use garage_table::*;
|
||||
use garage_util::error::Error as GarageError;
|
||||
|
||||
pub async fn run_web_server(
|
||||
garage: Arc<Garage>,
|
||||
shutdown_signal: impl Future<Output = ()>,
|
||||
) -> Result<(), GarageError> {
|
||||
let addr = &garage.config.s3_web.bind_addr;
|
||||
|
||||
let service = make_service_fn(|conn: &AddrStream| {
|
||||
let garage = garage.clone();
|
||||
let client_addr = conn.remote_addr();
|
||||
async move {
|
||||
Ok::<_, Error>(service_fn(move |req: Request<Body>| {
|
||||
let garage = garage.clone();
|
||||
handle_request(garage, req, client_addr)
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
let server = Server::bind(&addr).serve(service);
|
||||
let graceful = server.with_graceful_shutdown(shutdown_signal);
|
||||
info!("Web server listening on http://{}", addr);
|
||||
|
||||
graceful.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_request(
|
||||
garage: Arc<Garage>,
|
||||
req: Request<Body>,
|
||||
addr: SocketAddr,
|
||||
) -> Result<Response<Body>, Infallible> {
|
||||
info!("{} {} {}", addr, req.method(), req.uri());
|
||||
let res = serve_file(garage, req).await;
|
||||
match &res {
|
||||
Ok(r) => debug!("{} {:?}", r.status(), r.headers()),
|
||||
Err(e) => warn!("Response: error {}, {}", e.http_status_code(), e),
|
||||
}
|
||||
|
||||
Ok(res.unwrap_or_else(error_to_res))
|
||||
}
|
||||
|
||||
fn error_to_res(e: Error) -> Response<Body> {
|
||||
let body: Body = Body::from(format!("{}\n", e));
|
||||
let mut http_error = Response::new(body);
|
||||
*http_error.status_mut() = e.http_status_code();
|
||||
http_error
|
||||
}
|
||||
|
||||
async fn serve_file(garage: Arc<Garage>, req: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
// Get http authority string (eg. [::1]:3902 or garage.tld:80)
|
||||
let authority = req
|
||||
.headers()
|
||||
.get(HOST)
|
||||
.ok_or(Error::BadRequest(format!("HOST header required")))?
|
||||
.to_str()?;
|
||||
|
||||
// Get bucket
|
||||
let (host, _) = domain_to_unicode(authority_to_host(authority)?);
|
||||
let root = &garage.config.s3_web.root_domain;
|
||||
let bucket = host_to_bucket(&host, root);
|
||||
|
||||
// Check bucket is exposed as a website
|
||||
let bucket_desc = garage
|
||||
.bucket_table
|
||||
.get(&EmptyKey, &bucket.to_string())
|
||||
.await?
|
||||
.filter(|b| !b.is_deleted())
|
||||
.ok_or(Error::NotFound)?;
|
||||
|
||||
match bucket_desc.state.get() {
|
||||
BucketState::Present(params) if *params.website.get() => Ok(()),
|
||||
_ => Err(Error::NotFound),
|
||||
}?;
|
||||
|
||||
// Get path
|
||||
let path = req.uri().path().to_string();
|
||||
let index = &garage.config.s3_web.index;
|
||||
let key = path_to_key(&path, &index)?;
|
||||
|
||||
info!("Selected bucket: \"{}\", selected key: \"{}\"", bucket, key);
|
||||
|
||||
let res = match req.method() {
|
||||
&Method::HEAD => handle_head(garage, &bucket, &key).await?,
|
||||
&Method::GET => handle_get(garage, &req, bucket, &key).await?,
|
||||
_ => return Err(Error::BadRequest(format!("HTTP method not supported"))),
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Extract host from the authority section given by the HTTP host header
|
||||
///
|
||||
/// The HTTP host contains both a host and a port.
|
||||
/// Extracting the port is more complex than just finding the colon (:) symbol due to IPv6
|
||||
/// We do not use the collect pattern as there is no way in std rust to collect over a stack allocated value
|
||||
/// check here: https://docs.rs/collect_slice/1.2.0/collect_slice/
|
||||
fn authority_to_host(authority: &str) -> Result<&str, Error> {
|
||||
let mut iter = authority.chars().enumerate();
|
||||
let (_, first_char) = iter
|
||||
.next()
|
||||
.ok_or(Error::BadRequest(format!("Authority is empty")))?;
|
||||
|
||||
let split = match first_char {
|
||||
'[' => {
|
||||
let mut iter = iter.skip_while(|(_, c)| c != &']');
|
||||
match iter.next() {
|
||||
Some((_, ']')) => iter.next(),
|
||||
_ => {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Authority {} has an illegal format",
|
||||
authority
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => iter.skip_while(|(_, c)| c != &':').next(),
|
||||
};
|
||||
|
||||
match split {
|
||||
Some((i, ':')) => Ok(&authority[..i]),
|
||||
None => Ok(authority),
|
||||
Some((_, _)) => Err(Error::BadRequest(format!(
|
||||
"Authority {} has an illegal format",
|
||||
authority
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Host to bucket
|
||||
///
|
||||
/// Convert a host, like "bucket.garage-site.tld" or "john.doe.com"
|
||||
/// to the corresponding bucket, resp. "bucket" and "john.doe.com"
|
||||
/// considering that ".garage-site.tld" is the "root domain".
|
||||
/// This behavior has been chosen to follow AWS S3 semantic.
|
||||
fn host_to_bucket<'a>(host: &'a str, root: &str) -> &'a str {
|
||||
if root.len() >= host.len() || !host.ends_with(root) {
|
||||
return host;
|
||||
}
|
||||
|
||||
let len_diff = host.len() - root.len();
|
||||
let missing_starting_dot = root.chars().next() != Some('.');
|
||||
let cursor = if missing_starting_dot {
|
||||
len_diff - 1
|
||||
} else {
|
||||
len_diff
|
||||
};
|
||||
&host[..cursor]
|
||||
}
|
||||
|
||||
/// Path to key
|
||||
///
|
||||
/// Convert the provided path to the internal key
|
||||
/// When a path ends with "/", we append the index name to match traditional web server behavior
|
||||
/// which is also AWS S3 behavior.
|
||||
fn path_to_key<'a>(path: &'a str, index: &str) -> Result<Cow<'a, str>, Error> {
|
||||
let path_utf8 = percent_encoding::percent_decode_str(&path).decode_utf8()?;
|
||||
|
||||
if path_utf8.chars().next() != Some('/') {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Path must start with a / (slash)"
|
||||
)));
|
||||
}
|
||||
|
||||
match path_utf8.chars().last() {
|
||||
None => unreachable!(),
|
||||
Some('/') => {
|
||||
let mut key = String::with_capacity(path_utf8.len() + index.len());
|
||||
key.push_str(&path_utf8[1..]);
|
||||
key.push_str(index);
|
||||
Ok(key.into())
|
||||
}
|
||||
Some(_) => match path_utf8 {
|
||||
Cow::Borrowed(pu8) => Ok((&pu8[1..]).into()),
|
||||
Cow::Owned(pu8) => Ok((&pu8[1..]).to_string().into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn authority_to_host_with_port() -> Result<(), Error> {
|
||||
let domain = authority_to_host("[::1]:3902")?;
|
||||
assert_eq!(domain, "[::1]");
|
||||
let domain2 = authority_to_host("garage.tld:65200")?;
|
||||
assert_eq!(domain2, "garage.tld");
|
||||
let domain3 = authority_to_host("127.0.0.1:80")?;
|
||||
assert_eq!(domain3, "127.0.0.1");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authority_to_host_without_port() -> Result<(), Error> {
|
||||
let domain = authority_to_host("[::1]")?;
|
||||
assert_eq!(domain, "[::1]");
|
||||
let domain2 = authority_to_host("garage.tld")?;
|
||||
assert_eq!(domain2, "garage.tld");
|
||||
let domain3 = authority_to_host("127.0.0.1")?;
|
||||
assert_eq!(domain3, "127.0.0.1");
|
||||
assert!(authority_to_host("[").is_err());
|
||||
assert!(authority_to_host("[hello").is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_to_bucket_test() {
|
||||
assert_eq!(
|
||||
host_to_bucket("john.doe.garage.tld", ".garage.tld"),
|
||||
"john.doe"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
host_to_bucket("john.doe.garage.tld", "garage.tld"),
|
||||
"john.doe"
|
||||
);
|
||||
|
||||
assert_eq!(host_to_bucket("john.doe.com", "garage.tld"), "john.doe.com");
|
||||
|
||||
assert_eq!(
|
||||
host_to_bucket("john.doe.com", ".garage.tld"),
|
||||
"john.doe.com"
|
||||
);
|
||||
|
||||
assert_eq!(host_to_bucket("garage.tld", "garage.tld"), "garage.tld");
|
||||
|
||||
assert_eq!(host_to_bucket("garage.tld", ".garage.tld"), "garage.tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_to_key_test() -> Result<(), Error> {
|
||||
assert_eq!(path_to_key("/file%20.jpg", "index.html")?, "file .jpg");
|
||||
assert_eq!(path_to_key("/%20t/", "index.html")?, " t/index.html");
|
||||
assert_eq!(path_to_key("/", "index.html")?, "index.html");
|
||||
assert_eq!(path_to_key("/hello", "index.html")?, "hello");
|
||||
assert!(path_to_key("", "index.html").is_err());
|
||||
assert!(path_to_key("i/am/relative", "index.html").is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue