use std::fs::{self, Permissions}; use std::os::unix::prelude::PermissionsExt; use std::{convert::Infallible, sync::Arc}; use tokio::net::{TcpListener, UnixListener}; use tokio::sync::watch; use hyper::{ body::Body, body::Incoming as IncomingBody, header::{HeaderValue, HOST}, Method, Request, Response, StatusCode, }; use opentelemetry::{ global, metrics::{Counter, ValueRecorder}, trace::{FutureExt, TraceContextExt, Tracer}, Context, KeyValue, }; use crate::error::*; use garage_api_common::cors::{ add_cors_headers, find_matching_cors_rule, handle_options_for_bucket, }; use garage_api_common::generic_server::{server_loop, UnixListenerOn}; use garage_api_common::helpers::*; use garage_api_s3::api_server::ResBody; use garage_api_s3::error::{ CommonErrorDerivative, Error as ApiError, OkOrBadRequest, OkOrInternalError, }; use garage_api_s3::get::{handle_get_without_ctx, handle_head_without_ctx}; use garage_model::bucket_table::{self, RoutingRule}; use garage_model::garage::Garage; use garage_table::*; use garage_util::data::Uuid; use garage_util::error::Error as GarageError; use garage_util::forwarded_headers; use garage_util::metrics::{gen_trace_id, RecordDuration}; use garage_util::socket_address::UnixOrTCPSocketAddress; struct WebMetrics { request_counter: Counter, error_counter: Counter, request_duration: ValueRecorder, } impl WebMetrics { fn new() -> Self { let meter = global::meter("garage/web"); Self { request_counter: meter .u64_counter("web.request_counter") .with_description("Number of requests to the web endpoint") .init(), error_counter: meter .u64_counter("web.error_counter") .with_description("Number of requests to the web endpoint resulting in errors") .init(), request_duration: meter .f64_value_recorder("web.request_duration") .with_description("Duration of requests to the web endpoint") .init(), } } } pub struct WebServer { garage: Arc, metrics: Arc, root_domain: String, } impl WebServer { /// Run a web server pub fn new(garage: Arc, root_domain: String) -> Arc { let metrics = Arc::new(WebMetrics::new()); Arc::new(WebServer { garage, metrics, root_domain, }) } pub async fn run( self: Arc, bind_addr: UnixOrTCPSocketAddress, must_exit: watch::Receiver, ) -> Result<(), GarageError> { let server_name = "Web".into(); info!("Web server listening on {}", bind_addr); match bind_addr { UnixOrTCPSocketAddress::TCPSocket(addr) => { let listener = TcpListener::bind(addr).await?; let handler = move |stream, socketaddr| self.clone().handle_request(stream, socketaddr); server_loop(server_name, listener, handler, must_exit).await } UnixOrTCPSocketAddress::UnixSocket(ref path) => { if path.exists() { fs::remove_file(path)? } let listener = UnixListener::bind(path)?; let listener = UnixListenerOn(listener, path.display().to_string()); fs::set_permissions(path, Permissions::from_mode(0o222))?; let handler = move |stream, socketaddr| self.clone().handle_request(stream, socketaddr); server_loop(server_name, listener, handler, must_exit).await } } } async fn handle_request( self: Arc, req: Request, addr: String, ) -> Result>, http::Error> { if let Ok(forwarded_for_ip_addr) = forwarded_headers::handle_forwarded_for_headers(req.headers()) { info!( "{} (via {}) {} {}", forwarded_for_ip_addr, addr, req.method(), req.uri() ); } else { info!("{} {} {}", addr, req.method(), req.uri()); } // Lots of instrumentation let tracer = opentelemetry::global::tracer("garage"); let span = tracer .span_builder(format!("Web {} request", req.method())) .with_trace_id(gen_trace_id()) .with_attributes(vec![ KeyValue::new("method", format!("{}", req.method())), KeyValue::new("uri", req.uri().to_string()), ]) .start(&tracer); let metrics_tags = &[KeyValue::new("method", req.method().to_string())]; // The actual handler let res = self .serve_file(&req) .with_context(Context::current_with_span(span)) .record_duration(&self.metrics.request_duration, &metrics_tags[..]) .await; // More instrumentation self.metrics.request_counter.add(1, &metrics_tags[..]); // Returning the result match res { Ok(res) => { debug!("{} {} {}", req.method(), res.status(), req.uri()); Ok(res .map(|body| BoxBody::new(http_body_util::BodyExt::map_err(body, Error::from)))) } Err(error) => { info!( "{} {} {} {}", req.method(), error.http_status_code(), req.uri(), error ); self.metrics.error_counter.add( 1, &[ metrics_tags[0].clone(), KeyValue::new("status_code", error.http_status_code().to_string()), ], ); Ok(error_to_res(error)) } } } async fn check_key_exists(self: &Arc, bucket_id: Uuid, key: &str) -> Result { let exists = self .garage .object_table .get(&bucket_id, &key.to_string()) .await? .map(|object| object.versions().iter().any(|v| v.is_data())) .unwrap_or(false); Ok(exists) } async fn serve_file( self: &Arc, req: &Request, ) -> Result>, Error> { // Get http authority string (eg. [::1]:3902 or garage.tld:80) let authority = req .headers() .get(HOST) .ok_or_bad_request("HOST header required")? .to_str()?; // Get bucket let host = authority_to_host(authority)?; let bucket_name = host_to_bucket(&host, &self.root_domain).unwrap_or(&host); let bucket_id = self .garage .bucket_alias_table .get(&EmptyKey, &bucket_name.to_string()) .await? .and_then(|x| x.state.take()) .ok_or(Error::NotFound)?; // Check bucket isn't deleted and has website access enabled let bucket = self .garage .bucket_helper() .get_existing_bucket(bucket_id) .await .map_err(|_| Error::NotFound)?; let bucket_params = bucket.state.into_option().unwrap(); let website_config = bucket_params .website_config .get() .as_ref() .ok_or(Error::NotFound)?; // Get path let path = req.uri().path().to_string(); let index = &website_config.index_document; let routing_result = path_to_keys(&path, index, &website_config.routing_rules)?; debug!( "Selected bucket: \"{}\" {:?}, routing to {:?}", bucket_name, bucket_id, routing_result, ); let ret_doc = match (req.method(), routing_result.main_target()) { (&Method::OPTIONS, _) => handle_options_for_bucket(req, &bucket_params) .map_err(ApiError::from) .map(|res| res.map(|_empty_body: EmptyBody| empty_body())), (_, Err((url, code))) => Ok(Response::builder() .status(code) .header("Location", url) .body(empty_body()) .unwrap()), (_, Ok((key, code))) => { handle_inner(self.garage.clone(), req, bucket_id, key, code).await } }; // Try handling errors if bucket configuration provided fallbacks let ret_doc_with_redir = match (&ret_doc, &routing_result) { ( Err(ApiError::NoSuchKey), RoutingResult::LoadOrRedirect { redirect_if_exists, redirect_url, redirect_code, .. }, ) => { let redirect = if let Some(redirect_key) = redirect_if_exists { self.check_key_exists(bucket_id, redirect_key.as_str()) .await? } else { true }; if redirect { Ok(Response::builder() .status(redirect_code) .header("Location", redirect_url) .body(empty_body()) .unwrap()) } else { ret_doc } } ( Err(ApiError::NoSuchKey), RoutingResult::LoadOrAlternativeError { redirect_key, redirect_code, .. }, ) => { handle_inner( self.garage.clone(), req, bucket_id, redirect_key, *redirect_code, ) .await } _ => ret_doc, }; match ret_doc_with_redir.map_err(Error::from) { Err(error) => { // For a HEAD or OPTIONS method, and for non-4xx errors, // 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. if *req.method() == Method::HEAD || *req.method() == Method::OPTIONS || !error.http_status_code().is_client_error() { return Err(error); } // 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() .method("GET") .uri(format!("http://{}/{}", host, &error_document)) .body(empty_body::()) .unwrap(); match handle_inner( self.garage.clone(), &req2, bucket_id, &error_document, error.http_status_code(), ) .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 ); // 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) } 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(rule) = find_matching_cors_rule(&bucket_params, req)? { add_cors_headers(&mut resp, rule) .ok_or_internal_error("Invalid bucket CORS configuration")?; } Ok(resp) } } } } async fn handle_inner( garage: Arc, req: &Request, bucket_id: Uuid, key: &str, status_code: StatusCode, ) -> Result, ApiError> { if status_code != StatusCode::OK { // If we are returning an error document, discard all headers from // the original request that would have influenced the result: // - Range header, we don't want to return a subrange of the error document // - Caching directives such as If-None-Match, etc, which are not relevant let cleaned_req = Request::builder() .uri(req.uri()) .body(empty_body::()) .unwrap(); let mut ret = match req.method() { &Method::HEAD => { handle_head_without_ctx(garage, &cleaned_req, bucket_id, key, None).await? } &Method::GET => { handle_get_without_ctx( garage, &cleaned_req, bucket_id, key, None, Default::default(), ) .await? } _ => return Err(ApiError::bad_request("HTTP method not supported")), }; *ret.status_mut() = status_code; Ok(ret) } else { match req.method() { &Method::HEAD => handle_head_without_ctx(garage, req, bucket_id, key, None).await, &Method::GET => { handle_get_without_ctx(garage, req, bucket_id, key, None, Default::default()).await } _ => Err(ApiError::bad_request("HTTP method not supported")), } } } fn error_to_res(e: Error) -> Response> { // If we are here, it is either that: // - there was an error before trying to get the requested URL // from the bucket (e.g. bucket not found) // - there was an error processing the request and (the request // was a HEAD request or we couldn't get the error document) // We do NOT enter this code path when returning the bucket's // error document (this is handled in serve_file) let body = string_body(format!("{}\n", e)); let mut http_error = Response::new(body); *http_error.status_mut() = e.http_status_code(); e.add_headers(http_error.headers_mut()); http_error } #[derive(Debug, PartialEq)] enum RoutingResult { // Load a key and use `code` as status, or fallback to normal 404 handler if not found LoadKey { key: String, code: StatusCode, }, // Load a key and use `200` as status, or fallback with a redirection using `redirect_code` // as status LoadOrRedirect { key: String, redirect_if_exists: Option, redirect_url: String, redirect_code: StatusCode, }, // Load a key and use `200` as status, or fallback by loading a different key and use // `redirect_code` as status LoadOrAlternativeError { key: String, redirect_key: String, redirect_code: StatusCode, }, // Send an http redirect with `code` as status Redirect { url: String, code: StatusCode, }, } impl RoutingResult { // return Ok((key_to_deref, status_code)) or Err((redirect_target, status_code)) fn main_target(&self) -> Result<(&str, StatusCode), (&str, StatusCode)> { match self { RoutingResult::LoadKey { key, code } => Ok((key, *code)), RoutingResult::LoadOrRedirect { key, .. } => Ok((key, StatusCode::OK)), RoutingResult::LoadOrAlternativeError { key, .. } => Ok((key, StatusCode::OK)), RoutingResult::Redirect { url, code } => Err((url, *code)), } } } /// 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. /// /// Check: https://docs.aws.amazon.com/AmazonS3/latest/userguide/IndexDocumentSupport.html fn path_to_keys( path: &str, index: &str, routing_rules: &[RoutingRule], ) -> Result { let path_utf8 = percent_encoding::percent_decode_str(path).decode_utf8()?; let base_key = match path_utf8.strip_prefix("/") { Some(bk) => bk, None => return Err(Error::BadRequest("Path must start with a / (slash)".into())), }; let is_bucket_root = base_key.is_empty(); let is_trailing_slash = path_utf8.ends_with("/"); let key = if is_bucket_root || is_trailing_slash { // we can't store anything at the root, so we need to query the index // if the key end with a slash, we always query the index format!("{base_key}{index}") } else { // if the key doesn't end with `/`, leave it unmodified base_key.to_string() }; let mut routing_rules_iter = routing_rules.iter(); let key = loop { let Some(routing_rule) = routing_rules_iter.next() else { break key; }; let Ok(status_code) = StatusCode::from_u16(routing_rule.redirect.http_redirect_code) else { continue; }; if let Some(condition) = &routing_rule.condition { let suffix = if let Some(prefix) = &condition.prefix { let Some(suffix) = key.strip_prefix(prefix) else { continue; }; Some(suffix) } else { None }; let mut target = compute_redirect_target(&routing_rule.redirect, suffix); let query_alternative_key = status_code == StatusCode::OK || status_code == StatusCode::NOT_FOUND; let redirect_on_error = condition.http_error_code == Some(StatusCode::NOT_FOUND.as_u16()); match (query_alternative_key, redirect_on_error) { (false, false) => { return Ok(RoutingResult::Redirect { url: target, code: status_code, }) } (true, false) => { // we need to remove the leading / target.remove(0); if status_code == StatusCode::OK { break target; } else { return Ok(RoutingResult::LoadKey { key: target, code: status_code, }); } } (false, true) => { return Ok(RoutingResult::LoadOrRedirect { key, redirect_if_exists: None, redirect_url: target, redirect_code: status_code, }); } (true, true) => { target.remove(0); return Ok(RoutingResult::LoadOrAlternativeError { key, redirect_key: target, redirect_code: status_code, }); } } } else { let target = compute_redirect_target(&routing_rule.redirect, None); return Ok(RoutingResult::Redirect { url: target, code: status_code, }); } }; if is_bucket_root || is_trailing_slash { Ok(RoutingResult::LoadKey { key, code: StatusCode::OK, }) } else { Ok(RoutingResult::LoadOrRedirect { redirect_if_exists: Some(format!("{key}/{index}")), // we can't use `path` because key might have changed substentially in case of // routing rules redirect_url: percent_encoding::percent_encode( format!("/{key}/").as_bytes(), PATH_ENCODING_SET, ) .to_string(), key, redirect_code: StatusCode::FOUND, }) } } // per https://url.spec.whatwg.org/#path-percent-encode-set const PATH_ENCODING_SET: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS .add(b' ') .add(b'"') .add(b'#') .add(b'<') .add(b'>') .add(b'?') .add(b'`') .add(b'{') .add(b'}'); fn compute_redirect_target(redirect: &bucket_table::Redirect, suffix: Option<&str>) -> String { let mut res = String::new(); if let Some(hostname) = &redirect.hostname { if let Some(protocol) = &redirect.protocol { res.push_str(protocol); res.push_str("://"); } else { res.push_str("//"); } res.push_str(hostname); } res.push('/'); if let Some(replace_key_prefix) = &redirect.replace_key_prefix { res.push_str(replace_key_prefix); if let Some(suffix) = suffix { res.push_str(suffix) } } else if let Some(replace_key) = &redirect.replace_key { res.push_str(replace_key) } res } #[cfg(test)] mod tests { use super::*; #[test] fn path_to_keys_test() -> Result<(), Error> { assert_eq!( path_to_keys("/file%20.jpg", "index.html", &[])?, RoutingResult::LoadOrRedirect { key: "file .jpg".to_string(), redirect_url: "/file%20.jpg/".to_string(), redirect_if_exists: Some("file .jpg/index.html".to_string()), redirect_code: StatusCode::FOUND, } ); assert_eq!( path_to_keys("/%20t/", "index.html", &[])?, RoutingResult::LoadKey { key: " t/index.html".to_string(), code: StatusCode::OK } ); assert_eq!( path_to_keys("/", "index.html", &[])?, RoutingResult::LoadKey { key: "index.html".to_string(), code: StatusCode::OK } ); assert_eq!( path_to_keys("/hello", "index.html", &[])?, RoutingResult::LoadOrRedirect { key: "hello".to_string(), redirect_url: "/hello/".to_string(), redirect_if_exists: Some("hello/index.html".to_string()), redirect_code: StatusCode::FOUND, } ); assert!(path_to_keys("", "index.html", &[]).is_err()); assert!(path_to_keys("i/am/relative", "index.html", &[]).is_err()); Ok(()) } }