aboutsummaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
authorAlex <alex@adnab.me>2020-11-08 18:04:52 +0100
committerAlex <alex@adnab.me>2020-11-08 18:04:52 +0100
commit045009da9b7ac4198574bd5aa256c11cfe4ae469 (patch)
treed3014cc2739c5a1bce62ff35efc8417b79ff46ef /src/api
parent54166d2a09f488bff080469160d4df6a78db1a3f (diff)
parenta50fa70d45f8b5af68d23d60c3bac2af4ecceb58 (diff)
downloadgarage-045009da9b7ac4198574bd5aa256c11cfe4ae469.tar.gz
garage-045009da9b7ac4198574bd5aa256c11cfe4ae469.zip
Merge pull request 'Refactor error management in API part' (#10) from error-refactoring into master
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/10
Diffstat (limited to 'src/api')
-rw-r--r--src/api/Cargo.toml1
-rw-r--r--src/api/api_server.rs19
-rw-r--r--src/api/error.rs116
-rw-r--r--src/api/lib.rs2
-rw-r--r--src/api/s3_copy.rs3
-rw-r--r--src/api/s3_delete.rs5
-rw-r--r--src/api/s3_get.rs15
-rw-r--r--src/api/s3_put.rs32
-rw-r--r--src/api/signature.rs60
9 files changed, 174 insertions, 79 deletions
diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml
index 4e0599d5..578cb9d5 100644
--- a/src/api/Cargo.toml
+++ b/src/api/Cargo.toml
@@ -17,6 +17,7 @@ garage_util = { version = "0.1", path = "../util" }
garage_table = { version = "0.1.1", path = "../table" }
garage_model = { version = "0.1.1", path = "../model" }
+err-derive = "0.2.3"
bytes = "0.4"
hex = "0.3"
log = "0.4"
diff --git a/src/api/api_server.rs b/src/api/api_server.rs
index 9dc74dac..ec02572d 100644
--- a/src/api/api_server.rs
+++ b/src/api/api_server.rs
@@ -7,10 +7,11 @@ use hyper::server::conn::AddrStream;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server};
-use garage_util::error::Error;
+use garage_util::error::Error as GarageError;
use garage_model::garage::Garage;
+use crate::error::*;
use crate::signature::check_signature;
use crate::s3_copy::*;
@@ -22,14 +23,14 @@ use crate::s3_put::*;
pub async fn run_api_server(
garage: Arc<Garage>,
shutdown_signal: impl Future<Output = ()>,
-) -> Result<(), Error> {
+) -> Result<(), GarageError> {
let addr = &garage.config.s3_api.api_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>| {
+ Ok::<_, GarageError>(service_fn(move |req: Request<Body>| {
let garage = garage.clone();
handler(garage, req, client_addr)
}))
@@ -49,7 +50,7 @@ async fn handler(
garage: Arc<Garage>,
req: Request<Body>,
addr: SocketAddr,
-) -> Result<Response<Body>, Error> {
+) -> Result<Response<Body>, GarageError> {
info!("{} {} {}", addr, req.method(), req.uri());
debug!("{:?}", req);
match handler_inner(garage, req).await {
@@ -131,10 +132,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
source_bucket
)));
}
- let source_key = match source_key {
- None => return Err(Error::BadRequest(format!("No source key specified"))),
- Some(x) => x,
- };
+ let source_key = source_key.ok_or_bad_request("No source key specified")?;
Ok(handle_copy(garage, &bucket, &key, &source_bucket, &source_key).await?)
} else {
// PutObject query
@@ -205,9 +203,8 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
let max_keys = params
.get("max-keys")
.map(|x| {
- x.parse::<usize>().map_err(|e| {
- Error::BadRequest(format!("Invalid value for max-keys: {}", e))
- })
+ x.parse::<usize>()
+ .ok_or_bad_request("Invalid value for max-keys")
})
.unwrap_or(Ok(1000))?;
let prefix = params.get("prefix").map(|x| x.as_str()).unwrap_or(&"");
diff --git a/src/api/error.rs b/src/api/error.rs
new file mode 100644
index 00000000..ddb021db
--- /dev/null
+++ b/src/api/error.rs
@@ -0,0 +1,116 @@
+use err_derive::Error;
+use hyper::StatusCode;
+
+use garage_util::error::Error as GarageError;
+
+#[derive(Debug, Error)]
+pub enum Error {
+ // Category: internal error
+ #[error(display = "Internal error: {}", _0)]
+ InternalError(#[error(source)] GarageError),
+
+ #[error(display = "Internal error (Hyper error): {}", _0)]
+ Hyper(#[error(source)] hyper::Error),
+
+ #[error(display = "Internal error (HTTP error): {}", _0)]
+ HTTP(#[error(source)] http::Error),
+
+ // Category: cannot process
+ #[error(display = "Forbidden: {}", _0)]
+ Forbidden(String),
+
+ #[error(display = "Not found")]
+ NotFound,
+
+ // Category: bad request
+ #[error(display = "Invalid UTF-8: {}", _0)]
+ InvalidUTF8(#[error(source)] std::str::Utf8Error),
+
+ #[error(display = "Invalid XML: {}", _0)]
+ InvalidXML(#[error(source)] roxmltree::Error),
+
+ #[error(display = "Invalid header value: {}", _0)]
+ InvalidHeader(#[error(source)] hyper::header::ToStrError),
+
+ #[error(display = "Invalid HTTP range: {:?}", _0)]
+ InvalidRange(#[error(from)] http_range::HttpRangeParseError),
+
+ #[error(display = "Bad request: {}", _0)]
+ BadRequest(String),
+}
+
+impl Error {
+ pub fn http_status_code(&self) -> StatusCode {
+ match self {
+ Error::NotFound => StatusCode::NOT_FOUND,
+ Error::Forbidden(_) => StatusCode::FORBIDDEN,
+ Error::InternalError(GarageError::RPC(_)) => StatusCode::SERVICE_UNAVAILABLE,
+ Error::InternalError(_) | Error::Hyper(_) | Error::HTTP(_) => {
+ StatusCode::INTERNAL_SERVER_ERROR
+ }
+ _ => StatusCode::BAD_REQUEST,
+ }
+ }
+}
+
+pub trait OkOrBadRequest {
+ type S2;
+ fn ok_or_bad_request(self, reason: &'static str) -> Self::S2;
+}
+
+impl<T, E> OkOrBadRequest for Result<T, E>
+where
+ E: std::fmt::Display,
+{
+ type S2 = Result<T, Error>;
+ fn ok_or_bad_request(self, reason: &'static str) -> Result<T, Error> {
+ match self {
+ Ok(x) => Ok(x),
+ Err(e) => Err(Error::BadRequest(format!("{}: {}", reason, e))),
+ }
+ }
+}
+
+impl<T> OkOrBadRequest for Option<T> {
+ type S2 = Result<T, Error>;
+ fn ok_or_bad_request(self, reason: &'static str) -> Result<T, Error> {
+ match self {
+ Some(x) => Ok(x),
+ None => Err(Error::BadRequest(format!("{}", reason))),
+ }
+ }
+}
+
+pub trait OkOrInternalError {
+ type S2;
+ fn ok_or_internal_error(self, reason: &'static str) -> Self::S2;
+}
+
+impl<T, E> OkOrInternalError for Result<T, E>
+where
+ E: std::fmt::Display,
+{
+ type S2 = Result<T, Error>;
+ fn ok_or_internal_error(self, reason: &'static str) -> Result<T, Error> {
+ match self {
+ Ok(x) => Ok(x),
+ Err(e) => Err(Error::InternalError(GarageError::Message(format!(
+ "{}: {}",
+ reason, e
+ )))),
+ }
+ }
+}
+
+impl<T> OkOrInternalError for Option<T> {
+ type S2 = Result<T, Error>;
+ fn ok_or_internal_error(self, reason: &'static str) -> Result<T, Error> {
+ match self {
+ Some(x) => Ok(x),
+ None => Err(Error::InternalError(GarageError::Message(format!(
+ "{}",
+ reason
+ )))),
+ }
+ }
+}
diff --git a/src/api/lib.rs b/src/api/lib.rs
index df2fd045..9bb07925 100644
--- a/src/api/lib.rs
+++ b/src/api/lib.rs
@@ -1,6 +1,8 @@
#[macro_use]
extern crate log;
+pub mod error;
+
pub mod encoding;
pub mod api_server;
diff --git a/src/api/s3_copy.rs b/src/api/s3_copy.rs
index db790d95..4280f4bf 100644
--- a/src/api/s3_copy.rs
+++ b/src/api/s3_copy.rs
@@ -6,13 +6,14 @@ use hyper::{Body, Response};
use garage_table::*;
use garage_util::data::*;
-use garage_util::error::Error;
use garage_model::block_ref_table::*;
use garage_model::garage::Garage;
use garage_model::object_table::*;
use garage_model::version_table::*;
+use crate::error::*;
+
pub async fn handle_copy(
garage: Arc<Garage>,
dest_bucket: &str,
diff --git a/src/api/s3_delete.rs b/src/api/s3_delete.rs
index 42216f51..33e47c17 100644
--- a/src/api/s3_delete.rs
+++ b/src/api/s3_delete.rs
@@ -4,12 +4,12 @@ use std::sync::Arc;
use hyper::{Body, Request, Response};
use garage_util::data::*;
-use garage_util::error::Error;
use garage_model::garage::Garage;
use garage_model::object_table::*;
use crate::encoding::*;
+use crate::error::*;
async fn handle_delete_internal(
garage: &Garage,
@@ -85,8 +85,7 @@ pub async fn handle_delete_objects(
) -> Result<Response<Body>, Error> {
let body = hyper::body::to_bytes(req.into_body()).await?;
let cmd_xml = roxmltree::Document::parse(&std::str::from_utf8(&body)?)?;
- let cmd = parse_delete_objects_xml(&cmd_xml)
- .map_err(|e| Error::BadRequest(format!("Invald delete XML query: {}", e)))?;
+ let cmd = parse_delete_objects_xml(&cmd_xml).ok_or_bad_request("Invalid delete XML query")?;
let mut retxml = String::new();
writeln!(&mut retxml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap();
diff --git a/src/api/s3_get.rs b/src/api/s3_get.rs
index 68558dbe..71c656f2 100644
--- a/src/api/s3_get.rs
+++ b/src/api/s3_get.rs
@@ -5,13 +5,13 @@ use futures::stream::*;
use hyper::body::Bytes;
use hyper::{Body, Request, Response, StatusCode};
-use garage_util::error::Error;
-
use garage_table::EmptyKey;
use garage_model::garage::Garage;
use garage_model::object_table::*;
+use crate::error::*;
+
fn object_headers(
version: &ObjectVersion,
version_meta: &ObjectVersionMeta,
@@ -111,11 +111,8 @@ pub async fn handle_get(
let range = match req.headers().get("range") {
Some(range) => {
- let range_str = range
- .to_str()
- .map_err(|e| Error::BadRequest(format!("Invalid range header: {}", e)))?;
- let mut ranges = http_range::HttpRange::parse(range_str, last_v_meta.size)
- .map_err(|_e| Error::BadRequest(format!("Invalid range")))?;
+ let range_str = range.to_str()?;
+ let mut ranges = http_range::HttpRange::parse(range_str, last_v_meta.size)?;
if ranges.len() > 1 {
return Err(Error::BadRequest(format!("Multiple ranges not supported")));
} else {
@@ -210,7 +207,9 @@ pub async fn handle_get_range(
let body: Body = Body::from(bytes[begin as usize..end as usize].to_vec());
Ok(resp_builder.body(body)?)
} else {
- Err(Error::Message(format!("Internal error: requested range not present in inline bytes when it should have been")))
+ None.ok_or_internal_error(
+ "Requested range not present in inline bytes when it should have been",
+ )
}
}
ObjectVersionData::FirstBlock(_meta, _first_block_hash) => {
diff --git a/src/api/s3_put.rs b/src/api/s3_put.rs
index 0926ba89..ea09524c 100644
--- a/src/api/s3_put.rs
+++ b/src/api/s3_put.rs
@@ -9,8 +9,9 @@ use sha2::{Digest as Sha256Digest, Sha256};
use garage_table::*;
use garage_util::data::*;
-use garage_util::error::Error;
+use garage_util::error::Error as GarageError;
+use crate::error::*;
use garage_model::block::INLINE_THRESHOLD;
use garage_model::block_ref_table::*;
use garage_model::garage::Garage;
@@ -85,7 +86,7 @@ pub async fn handle_put(
// Validate MD5 sum against content-md5 header and sha256sum against signed content-sha256
if let Some(expected_sha256) = content_sha256 {
if expected_sha256 != sha256sum {
- return Err(Error::Message(format!(
+ return Err(Error::BadRequest(format!(
"Unable to validate x-amz-content-sha256"
)));
} else {
@@ -94,7 +95,7 @@ pub async fn handle_put(
}
if let Some(expected_md5) = content_md5 {
if expected_md5.trim_matches('"') != md5sum {
- return Err(Error::Message(format!("Unable to validate content-md5")));
+ return Err(Error::BadRequest(format!("Unable to validate content-md5")));
} else {
trace!("Successfully validated content-md5");
}
@@ -184,7 +185,7 @@ async fn put_block_meta(
offset: u64,
hash: Hash,
size: u64,
-) -> Result<(), Error> {
+) -> Result<(), GarageError> {
// TODO: don't clone, restart from empty block list ??
let mut version = version.clone();
version
@@ -225,7 +226,7 @@ impl BodyChunker {
buf: VecDeque::new(),
}
}
- async fn next(&mut self) -> Result<Option<Vec<u8>>, Error> {
+ async fn next(&mut self) -> Result<Option<Vec<u8>>, GarageError> {
while !self.read_all && self.buf.len() < self.block_size {
if let Some(block) = self.body.next().await {
let bytes = block?;
@@ -305,10 +306,9 @@ pub async fn handle_put_part(
// Check parameters
let part_number = part_number_str
.parse::<u64>()
- .map_err(|e| Error::BadRequest(format!("Invalid part number: {}", e)))?;
+ .ok_or_bad_request("Invalid part number")?;
- let version_uuid =
- uuid_from_str(upload_id).map_err(|_| Error::BadRequest(format!("Invalid upload ID")))?;
+ let version_uuid = decode_upload_id(upload_id)?;
let content_md5 = match req.headers().get("content-md5") {
Some(x) => Some(x.to_str()?.to_string()),
@@ -359,7 +359,7 @@ pub async fn handle_put_part(
// Validate MD5 sum against content-md5 header and sha256sum against signed content-sha256
if let Some(expected_sha256) = content_sha256 {
if expected_sha256 != sha256sum {
- return Err(Error::Message(format!(
+ return Err(Error::BadRequest(format!(
"Unable to validate x-amz-content-sha256"
)));
} else {
@@ -368,7 +368,7 @@ pub async fn handle_put_part(
}
if let Some(expected_md5) = content_md5 {
if expected_md5.trim_matches('"') != md5sum {
- return Err(Error::Message(format!("Unable to validate content-md5")));
+ return Err(Error::BadRequest(format!("Unable to validate content-md5")));
} else {
trace!("Successfully validated content-md5");
}
@@ -384,8 +384,7 @@ pub async fn handle_complete_multipart_upload(
key: &str,
upload_id: &str,
) -> Result<Response<Body>, Error> {
- let version_uuid =
- uuid_from_str(upload_id).map_err(|_| Error::BadRequest(format!("Invalid upload ID")))?;
+ let version_uuid = decode_upload_id(upload_id)?;
let bucket = bucket.to_string();
let key = key.to_string();
@@ -469,8 +468,7 @@ pub async fn handle_abort_multipart_upload(
key: &str,
upload_id: &str,
) -> Result<Response<Body>, Error> {
- let version_uuid =
- uuid_from_str(upload_id).map_err(|_| Error::BadRequest(format!("Invalid upload ID")))?;
+ let version_uuid = decode_upload_id(upload_id)?;
let object = garage
.object_table
@@ -532,10 +530,10 @@ fn get_headers(req: &Request<Body>) -> Result<ObjectVersionHeaders, Error> {
})
}
-fn uuid_from_str(id: &str) -> Result<UUID, ()> {
- let id_bin = hex::decode(id).map_err(|_| ())?;
+fn decode_upload_id(id: &str) -> Result<UUID, Error> {
+ let id_bin = hex::decode(id).ok_or_bad_request("Invalid upload ID")?;
if id_bin.len() != 32 {
- return Err(());
+ return None.ok_or_bad_request("Invalid upload ID");
}
let mut uuid = [0u8; 32];
uuid.copy_from_slice(&id_bin[..]);
diff --git a/src/api/signature.rs b/src/api/signature.rs
index 6e23afda..402b1881 100644
--- a/src/api/signature.rs
+++ b/src/api/signature.rs
@@ -7,12 +7,12 @@ use sha2::{Digest, Sha256};
use garage_table::*;
use garage_util::data::Hash;
-use garage_util::error::Error;
use garage_model::garage::Garage;
use garage_model::key_table::*;
use crate::encoding::uri_encode;
+use crate::error::*;
const SHORT_DATE: &str = "%Y%m%d";
const LONG_DATETIME: &str = "%Y%m%dT%H%M%SZ";
@@ -42,9 +42,9 @@ pub async fn check_signature(
let date = headers
.get("x-amz-date")
- .ok_or(Error::BadRequest("Missing X-Amz-Date field".into()))?;
+ .ok_or_bad_request("Missing X-Amz-Date field")?;
let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, LONG_DATETIME)
- .map_err(|e| Error::BadRequest(format!("Invalid date: {}", e)))?
+ .ok_or_bad_request("Invalid date")?
.into();
let date: DateTime<Utc> = DateTime::from_utc(date, Utc);
@@ -90,7 +90,7 @@ pub async fn check_signature(
&garage.config.s3_api.s3_region,
"s3",
)
- .map_err(|e| Error::Message(format!("Unable to build signing HMAC: {}", e)))?;
+ .ok_or_internal_error("Unable to build signing HMAC")?;
hmac.input(string_to_sign.as_bytes());
let signature = hex::encode(hmac.result().code());
@@ -104,9 +104,8 @@ pub async fn check_signature(
let content_sha256 = if authorization.content_sha256 == "UNSIGNED-PAYLOAD" {
None
} else {
- let bytes = hex::decode(authorization.content_sha256).or(Err(Error::BadRequest(
- format!("Invalid content sha256 hash"),
- )))?;
+ let bytes = hex::decode(authorization.content_sha256)
+ .ok_or_bad_request("Invalid content sha256 hash")?;
let mut hash = [0u8; 32];
if bytes.len() != 32 {
return Err(Error::BadRequest(format!("Invalid content sha256 hash")));
@@ -132,7 +131,7 @@ fn parse_authorization(
) -> Result<Authorization, Error> {
let first_space = authorization
.find(' ')
- .ok_or(Error::BadRequest("Authorization field too short".into()))?;
+ .ok_or_bad_request("Authorization field to short")?;
let (auth_kind, rest) = authorization.split_at(first_space);
if auth_kind != "AWS4-HMAC-SHA256" {
@@ -142,41 +141,32 @@ fn parse_authorization(
let mut auth_params = HashMap::new();
for auth_part in rest.split(',') {
let auth_part = auth_part.trim();
- let eq = auth_part.find('=').ok_or(Error::BadRequest(format!(
- "Missing =value in authorization field {}",
- auth_part
- )))?;
+ let eq = auth_part
+ .find('=')
+ .ok_or_bad_request("Field without value in authorization header")?;
let (key, value) = auth_part.split_at(eq);
auth_params.insert(key.to_string(), value.trim_start_matches('=').to_string());
}
let cred = auth_params
.get("Credential")
- .ok_or(Error::BadRequest(format!(
- "Could not find Credential in Authorization field"
- )))?;
+ .ok_or_bad_request("Could not find Credential in Authorization field")?;
let (key_id, scope) = parse_credential(cred)?;
let content_sha256 = headers
.get("x-amz-content-sha256")
- .ok_or(Error::BadRequest(
- "Missing X-Amz-Content-Sha256 field".into(),
- ))?;
+ .ok_or_bad_request("Missing X-Amz-Content-Sha256 field")?;
let auth = Authorization {
key_id,
scope,
signed_headers: auth_params
.get("SignedHeaders")
- .ok_or(Error::BadRequest(format!(
- "Could not find SignedHeaders in Authorization field"
- )))?
+ .ok_or_bad_request("Could not find SignedHeaders in Authorization field")?
.to_string(),
signature: auth_params
.get("Signature")
- .ok_or(Error::BadRequest(format!(
- "Could not find Signature in Authorization field"
- )))?
+ .ok_or_bad_request("Could not find Signature in Authorization field")?
.to_string(),
content_sha256: content_sha256.to_string(),
};
@@ -186,9 +176,7 @@ fn parse_authorization(
fn parse_query_authorization(headers: &HashMap<String, String>) -> Result<Authorization, Error> {
let algo = headers
.get("x-amz-algorithm")
- .ok_or(Error::BadRequest(format!(
- "X-Amz-Algorithm not found in query parameters"
- )))?;
+ .ok_or_bad_request("X-Amz-Algorithm not found in query parameters")?;
if algo != "AWS4-HMAC-SHA256" {
return Err(Error::BadRequest(format!(
"Unsupported authorization method"
@@ -197,20 +185,14 @@ fn parse_query_authorization(headers: &HashMap<String, String>) -> Result<Author
let cred = headers
.get("x-amz-credential")
- .ok_or(Error::BadRequest(format!(
- "X-Amz-Credential not found in query parameters"
- )))?;
+ .ok_or_bad_request("X-Amz-Credential not found in query parameters")?;
let (key_id, scope) = parse_credential(cred)?;
let signed_headers = headers
.get("x-amz-signedheaders")
- .ok_or(Error::BadRequest(format!(
- "X-Amz-SignedHeaders not found in query parameters"
- )))?;
+ .ok_or_bad_request("X-Amz-SignedHeaders not found in query parameters")?;
let signature = headers
.get("x-amz-signature")
- .ok_or(Error::BadRequest(format!(
- "X-Amz-Signature not found in query parameters"
- )))?;
+ .ok_or_bad_request("X-Amz-Signature not found in query parameters")?;
let content_sha256 = headers
.get("x-amz-content-sha256")
.map(|x| x.as_str())
@@ -226,9 +208,9 @@ fn parse_query_authorization(headers: &HashMap<String, String>) -> Result<Author
}
fn parse_credential(cred: &str) -> Result<(String, String), Error> {
- let first_slash = cred.find('/').ok_or(Error::BadRequest(format!(
- "Credentials does not contain / in authorization field"
- )))?;
+ let first_slash = cred
+ .find('/')
+ .ok_or_bad_request("Credentials does not contain / in authorization field")?;
let (key_id, scope) = cred.split_at(first_slash);
Ok((
key_id.to_string(),