diff options
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/Cargo.toml | 1 | ||||
-rw-r--r-- | src/api/api_server.rs | 19 | ||||
-rw-r--r-- | src/api/error.rs | 116 | ||||
-rw-r--r-- | src/api/helpers.rs | 4 | ||||
-rw-r--r-- | src/api/lib.rs | 2 | ||||
-rw-r--r-- | src/api/s3_copy.rs | 3 | ||||
-rw-r--r-- | src/api/s3_delete.rs | 5 | ||||
-rw-r--r-- | src/api/s3_get.rs | 15 | ||||
-rw-r--r-- | src/api/s3_put.rs | 32 | ||||
-rw-r--r-- | src/api/signature.rs | 60 |
10 files changed, 176 insertions, 81 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 93bed8bb..cdf8fa78 100644 --- a/src/api/api_server.rs +++ b/src/api/api_server.rs @@ -8,10 +8,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::*; @@ -24,14 +25,14 @@ use crate::helpers::*; 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) })) @@ -56,7 +57,7 @@ async fn handler(garage: Arc<Garage>, req: Request<Body>, client_addr: SocketAdd .make_infallible() } -async fn controller(garage: Arc<Garage>, req: Request<Body>) -> Result<Response<Body>, Error> { +async fn controller(garage: Arc<Garage>, req: Request<Body>) -> Result<Response<Body>, GarageError> { let path = req.uri().path().to_string(); let path = percent_encoding::percent_decode_str(&path).decode_utf8()?; @@ -120,10 +121,7 @@ async fn controller(garage: Arc<Garage>, req: Request<Body>) -> Result<Response< 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 @@ -194,9 +192,8 @@ async fn controller(garage: Arc<Garage>, req: Request<Body>) -> Result<Response< 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/helpers.rs b/src/api/helpers.rs index 19018151..f4d91788 100644 --- a/src/api/helpers.rs +++ b/src/api/helpers.rs @@ -2,13 +2,13 @@ use std::convert::Infallible; use std::net::SocketAddr; use hyper::{Body, Response}; -use garage_util::error::Error; +use garage_util::error::Error as GarageError; pub trait InfallibleResult { fn make_infallible(self) -> Result<Response<Body>, Infallible>; } -impl InfallibleResult for Result<Response<Body>, Error> { +impl InfallibleResult for Result<Response<Body>, GarageError> { fn make_infallible(self) -> Result<Response<Body>, Infallible> { match self { Ok(x) => { diff --git a/src/api/lib.rs b/src/api/lib.rs index 003780c1..7327354f 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(), |