diff options
Diffstat (limited to 'src/web')
-rw-r--r-- | src/web/web_server.rs | 175 |
1 files changed, 121 insertions, 54 deletions
diff --git a/src/web/web_server.rs b/src/web/web_server.rs index 834f31e8..bf330e85 100644 --- a/src/web/web_server.rs +++ b/src/web/web_server.rs @@ -2,19 +2,22 @@ use std::{borrow::Cow, convert::Infallible, net::SocketAddr, sync::Arc}; use futures::future::Future; +use http::header::{ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD}; use hyper::{ header::{HeaderValue, HOST}, server::conn::AddrStream, service::{make_service_fn, service_fn}, - Body, Method, Request, Response, Server, + Body, Method, Request, Response, Server, StatusCode, }; 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::s3_cors::{add_cors_headers, cors_rule_matches}; use garage_api::s3_get::{handle_get, handle_head}; +use garage_model::bucket_table::Bucket; use garage_model::garage::Garage; use garage_table::*; @@ -132,72 +135,136 @@ async fn serve_file(garage: Arc<Garage>, req: &Request<Body>) -> Result<Response ); let ret_doc = match *req.method() { - Method::HEAD => handle_head(garage.clone(), req, bucket_id, &key).await, + Method::OPTIONS => return handle_options(&bucket, req), + Method::HEAD => { + return handle_head(garage.clone(), req, bucket_id, &key) + .await + .map_err(Error::from) + } Method::GET => handle_get(garage.clone(), req, bucket_id, &key).await, _ => Err(ApiError::BadRequest("HTTP method not supported".into())), } .map_err(Error::from); - if let Err(error) = ret_doc { - 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); - } + match ret_doc { + Err(error) => { + // For a HEAD or OPTIONS method, 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. + assert!(*req.method() != Method::HEAD && *req.method() != Method::OPTIONS); + + if !error.http_status_code().is_client_error() { + // Do not return the error document if it is not a 4xx error code. + return Err(error); + } - // Same if no error document is set: just return the error directly - let error_document = match &website_config.error_document { - Some(ed) => ed.trim_start_matches('/').to_owned(), - None => return Err(error), - }; - - // We want to return the error document - // Create a fake HTTP request with path = the error document - let req2 = Request::builder() - .uri(format!("http://{}/{}", host, &error_document)) - .body(Body::empty()) - .unwrap(); - - match handle_get(garage, &req2, bucket_id, &error_document).await { - Ok(mut error_doc) => { - // The error won't be logged back in handle_request, - // so log it here - info!( - "{} {} {} {}", - req.method(), - req.uri(), - error.http_status_code(), - error - ); - - *error_doc.status_mut() = error.http_status_code(); - error.add_headers(error_doc.headers_mut()); - - // Preserve error message in a special header - for error_line in error.to_string().split('\n') { - if let Ok(v) = HeaderValue::from_bytes(error_line.as_bytes()) { - error_doc.headers_mut().append("X-Garage-Error", v); + // If no error document is set: just return the error directly + let error_document = match &website_config.error_document { + Some(ed) => ed.trim_start_matches('/').to_owned(), + None => return Err(error), + }; + + // We want to return the error document + // Create a fake HTTP request with path = the error document + let req2 = Request::builder() + .uri(format!("http://{}/{}", host, &error_document)) + .body(Body::empty()) + .unwrap(); + + match handle_get(garage, &req2, bucket_id, &error_document).await { + Ok(mut error_doc) => { + // The error won't be logged back in handle_request, + // so log it here + info!( + "{} {} {} {}", + req.method(), + req.uri(), + error.http_status_code(), + error + ); + + *error_doc.status_mut() = error.http_status_code(); + error.add_headers(error_doc.headers_mut()); + + // Preserve error message in a special header + for error_line in error.to_string().split('\n') { + if let Ok(v) = HeaderValue::from_bytes(error_line.as_bytes()) { + error_doc.headers_mut().append("X-Garage-Error", v); + } } - } - Ok(error_doc) + Ok(error_doc) + } + Err(error_doc_error) => { + warn!( + "Couldn't get error document {} for bucket {:?}: {}", + error_document, bucket_id, error_doc_error + ); + Err(error) + } } - Err(error_doc_error) => { - warn!( - "Couldn't get error document {} for bucket {:?}: {}", - error_document, bucket_id, error_doc_error - ); - Err(error) + } + Ok(mut resp) => { + // Maybe add CORS headers + 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![], + }; + let matching_rule = cors_config.iter().find(|rule| { + cors_rule_matches( + rule, + origin, + &req.method().to_string(), + request_headers.iter(), + ) + }); + if let Some(rule) = matching_rule { + add_cors_headers(&mut resp, rule) + .ok_or_internal_error("Invalid CORS configuration")?; + } + } } + Ok(resp) } - } else { - ret_doc } } +fn handle_options(bucket: &Bucket, req: &Request<Body>) -> Result<Response<Body>, Error> { + 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()) + .map_err(ApiError::from)?; + add_cors_headers(&mut resp, rule).ok_or_internal_error("Invalid CORS configuration")?; + return Ok(resp); + } + } + + Err(ApiError::Forbidden("No matching CORS rule".into()).into()) +} + /// Path to key /// /// Convert the provided path to the internal key |