From 4a5360aef4d395c095216839028c79d4bdcdbb18 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 24 Jan 2023 14:54:32 +0100 Subject: [PATCH] rest-server: drop Router from ApiConfig instead, allow attaching routers to path prefixes and also add an optional non-formatting router Signed-off-by: Wolfgang Bumiller --- proxmox-rest-server/src/api_config.rs | 52 ++- proxmox-rest-server/src/environment.rs | 2 +- proxmox-rest-server/src/formatter.rs | 44 +- proxmox-rest-server/src/h2service.rs | 4 +- proxmox-rest-server/src/lib.rs | 37 +- proxmox-rest-server/src/rest.rs | 544 +++++++++++++++++++------ 6 files changed, 518 insertions(+), 165 deletions(-) diff --git a/proxmox-rest-server/src/api_config.rs b/proxmox-rest-server/src/api_config.rs index b75cebd0..ba21fcd3 100644 --- a/proxmox-rest-server/src/api_config.rs +++ b/proxmox-rest-server/src/api_config.rs @@ -5,22 +5,23 @@ use std::sync::{Arc, Mutex}; use anyhow::{format_err, Error}; use hyper::http::request::Parts; -use hyper::{Body, Method, Response}; +use hyper::{Body, Response}; -use proxmox_router::{ApiMethod, Router, RpcEnvironmentType, UserInformation}; +use proxmox_router::{Router, RpcEnvironmentType, UserInformation}; use proxmox_sys::fs::{create_path, CreateOptions}; +use crate::rest::Handler; use crate::{AuthError, CommandSocket, FileLogOptions, FileLogger, RestEnvironment, ServerAdapter}; /// REST server configuration pub struct ApiConfig { basedir: PathBuf, - router: &'static Router, aliases: HashMap, env_type: RpcEnvironmentType, request_log: Option>>, auth_log: Option>>, adapter: Pin>, + handlers: Vec, #[cfg(feature = "templates")] templates: templates::Templates, @@ -31,8 +32,6 @@ impl ApiConfig { /// /// `basedir` - File lookups are relative to this directory. /// - /// `router` - The REST API definition. - /// /// `env_type` - The environment type. /// /// `api_auth` - The Authentication handler @@ -43,18 +42,17 @@ impl ApiConfig { /// ([render_template](Self::render_template) to generate pages. pub fn new>( basedir: B, - router: &'static Router, env_type: RpcEnvironmentType, adapter: impl ServerAdapter + 'static, ) -> Self { Self { basedir: basedir.into(), - router, aliases: HashMap::new(), env_type, request_log: None, auth_log: None, adapter: Box::pin(adapter), + handlers: Vec::new(), #[cfg(feature = "templates")] templates: Default::default(), @@ -77,15 +75,6 @@ impl ApiConfig { self.adapter.check_auth(headers, method).await } - pub(crate) fn find_method( - &self, - components: &[&str], - method: Method, - uri_param: &mut HashMap, - ) -> Option<&'static ApiMethod> { - self.router.find_method(components, method, uri_param) - } - pub(crate) fn find_alias(&self, mut components: &[&str]) -> PathBuf { let mut filename = self.basedir.clone(); if components.is_empty() { @@ -233,6 +222,37 @@ impl ApiConfig { pub(crate) fn get_auth_log(&self) -> Option<&Arc>> { self.auth_log.as_ref() } + + pub(crate) fn find_handler<'a>(&'a self, path_components: &[&str]) -> Option<&'a Handler> { + self.handlers + .iter() + .find(|handler| path_components.strip_prefix(handler.prefix).is_some()) + } + + pub fn add_default_api2_handler(&mut self, router: &'static Router) -> &mut Self { + self.handlers.push(Handler::default_api2_handler(router)); + self + } + + pub fn add_formatted_router( + &mut self, + prefix: &'static [&'static str], + router: &'static Router, + ) -> &mut Self { + self.handlers + .push(Handler::formatted_router(prefix, router)); + self + } + + pub fn add_unformatted_router( + &mut self, + prefix: &'static [&'static str], + router: &'static Router, + ) -> &mut Self { + self.handlers + .push(Handler::unformatted_router(prefix, router)); + self + } } #[cfg(feature = "templates")] diff --git a/proxmox-rest-server/src/environment.rs b/proxmox-rest-server/src/environment.rs index b4dff76b..ca41bb50 100644 --- a/proxmox-rest-server/src/environment.rs +++ b/proxmox-rest-server/src/environment.rs @@ -9,7 +9,7 @@ use crate::ApiConfig; /// Encapsulates information about the runtime environment pub struct RestEnvironment { - env_type: RpcEnvironmentType, + pub(crate) env_type: RpcEnvironmentType, result_attributes: Value, auth_id: Option, client_ip: Option, diff --git a/proxmox-rest-server/src/formatter.rs b/proxmox-rest-server/src/formatter.rs index 2e9a01fa..d05ddd93 100644 --- a/proxmox-rest-server/src/formatter.rs +++ b/proxmox-rest-server/src/formatter.rs @@ -133,29 +133,33 @@ impl OutputFormatter for JsonFormatter { } fn format_error(&self, err: Error) -> Response { - let mut response = if let Some(apierr) = err.downcast_ref::() { - let mut resp = Response::new(Body::from(apierr.message.clone())); - *resp.status_mut() = apierr.code; - resp - } else { - let mut resp = Response::new(Body::from(err.to_string())); - *resp.status_mut() = StatusCode::BAD_REQUEST; - resp - }; - - response.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static(JSON_CONTENT_TYPE), - ); - - response - .extensions_mut() - .insert(ErrorMessageExtension(err.to_string())); - - response + error_to_response(err) } } +pub(crate) fn error_to_response(err: Error) -> Response { + let mut response = if let Some(apierr) = err.downcast_ref::() { + let mut resp = Response::new(Body::from(apierr.message.clone())); + *resp.status_mut() = apierr.code; + resp + } else { + let mut resp = Response::new(Body::from(err.to_string())); + *resp.status_mut() = StatusCode::BAD_REQUEST; + resp + }; + + response.headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static(JSON_CONTENT_TYPE), + ); + + response + .extensions_mut() + .insert(ErrorMessageExtension(err.to_string())); + + response +} + /// Format data as ExtJS compatible ``application/json`` /// /// The returned json object contains the following properties: diff --git a/proxmox-rest-server/src/h2service.rs b/proxmox-rest-server/src/h2service.rs index 3f90c178..84a30985 100644 --- a/proxmox-rest-server/src/h2service.rs +++ b/proxmox-rest-server/src/h2service.rs @@ -12,7 +12,7 @@ use proxmox_router::http_err; use proxmox_router::{ApiResponseFuture, HttpError, Router, RpcEnvironment}; use crate::formatter::*; -use crate::{normalize_uri_path, WorkerTask}; +use crate::{normalize_path_with_components, WorkerTask}; /// Hyper Service implementation to handle stateful H2 connections. /// @@ -47,7 +47,7 @@ impl H2Service { let method = parts.method.clone(); - let (path, components) = match normalize_uri_path(parts.uri.path()) { + let (path, components) = match normalize_path_with_components(parts.uri.path()) { Ok((p, c)) => (p, c), Err(err) => return future::err(http_err!(BAD_REQUEST, "{}", err)).boxed(), }; diff --git a/proxmox-rest-server/src/lib.rs b/proxmox-rest-server/src/lib.rs index ce274409..a5cce380 100644 --- a/proxmox-rest-server/src/lib.rs +++ b/proxmox-rest-server/src/lib.rs @@ -15,6 +15,7 @@ //! - worker task management //! * generic interface to authenticate user +use std::fmt; use std::future::Future; use std::os::unix::io::{FromRawFd, OwnedFd}; use std::pin::Pin; @@ -222,7 +223,9 @@ pub fn cookie_from_header(headers: &http::HeaderMap, cookie_name: &str) -> Optio /// /// Do not allow ".", "..", or hidden files ".XXXX" /// Also remove empty path components -pub fn normalize_uri_path(path: &str) -> Result<(String, Vec<&str>), Error> { +pub fn normalize_path_with_components( + path: &str, +) -> Result<(String, Vec<&str>), IllegalPathComponents> { let items = path.split('/'); let mut path = String::new(); @@ -233,7 +236,7 @@ pub fn normalize_uri_path(path: &str) -> Result<(String, Vec<&str>), Error> { continue; } if name.starts_with('.') { - bail!("Path contains illegal components."); + return Err(IllegalPathComponents); } path.push('/'); path.push_str(name); @@ -242,3 +245,33 @@ pub fn normalize_uri_path(path: &str) -> Result<(String, Vec<&str>), Error> { Ok((path, components)) } + +#[derive(Debug)] +pub struct IllegalPathComponents; + +impl std::error::Error for IllegalPathComponents {} + +impl fmt::Display for IllegalPathComponents { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("path contains illegal components") + } +} + +/// Normalize a uri path by stripping empty components. +/// Components starting with a '.' are illegal. +pub fn normalize_path(path: &str) -> Result { + let mut output = String::with_capacity(path.len()); + for item in path.split('/') { + if item.is_empty() { + continue; + } + + if item.starts_with('.') { + return Err(IllegalPathComponents); + } + + output.push('/'); + output.push_str(item); + } + Ok(output) +} diff --git a/proxmox-rest-server/src/rest.rs b/proxmox-rest-server/src/rest.rs index 7a31857b..833e2fb6 100644 --- a/proxmox-rest-server/src/rest.rs +++ b/proxmox-rest-server/src/rest.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::future::Future; use std::hash::BuildHasher; +use std::io; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::{Arc, Mutex}; @@ -32,7 +33,7 @@ use proxmox_async::stream::AsyncReaderStream; use proxmox_compression::{DeflateEncoder, Level}; use crate::{ - formatter::*, normalize_uri_path, ApiConfig, AuthError, CompressionMethod, FileLogger, + formatter::*, normalize_path, ApiConfig, AuthError, CompressionMethod, FileLogger, RestEnvironment, }; @@ -42,7 +43,7 @@ extern "C" { struct AuthStringExtension(String); -struct EmptyUserInformation {} +pub(crate) struct EmptyUserInformation {} impl UserInformation for EmptyUserInformation { fn is_superuser(&self, _userid: &str) -> bool { @@ -90,7 +91,7 @@ impl Service<&T> for RestServer { Err(err) => Err(format_err!("unable to get peer address - {}", err)), Ok(peer) => Ok(ApiService { peer, - api_config: self.api_config.clone(), + api_config: Arc::clone(&self.api_config), }), }) } @@ -387,6 +388,14 @@ async fn proxy_protected_request( Ok(resp) } +fn delay_unauth_time() -> std::time::Instant { + std::time::Instant::now() + std::time::Duration::from_millis(3000) +} + +fn access_forbidden_time() -> std::time::Instant { + std::time::Instant::now() + std::time::Duration::from_millis(500) +} + pub(crate) async fn handle_api_request( mut rpcenv: Env, info: &'static ApiMethod, @@ -395,7 +404,6 @@ pub(crate) async fn handle_api_request, ) -> Result, Error> { - let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000); let compression = extract_compression_method(&parts.headers); let result = match info.handler { @@ -438,7 +446,7 @@ pub(crate) async fn handle_api_request { if let Some(httperr) = err.downcast_ref::() { if httperr.code == StatusCode::UNAUTHORIZED { - tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await; + tokio::time::sleep_until(Instant::from_std(delay_unauth_time())).await; } } formatter.format_error(err) @@ -472,6 +480,100 @@ pub(crate) async fn handle_api_request( + mut rpcenv: Env, + info: &'static ApiMethod, + parts: Parts, + req_body: Body, + uri_param: HashMap, +) -> Result, Error> { + let compression = extract_compression_method(&parts.headers); + + fn to_json_response( + value: Value, + env: &Env, + ) -> Result, Error> { + if let Some(attr) = env.result_attrib().as_object() { + if !attr.is_empty() { + http_bail!( + INTERNAL_SERVER_ERROR, + "result attributes are no longer supported" + ); + } + } + let value = serde_json::to_string(&value)?; + Ok(Response::builder().status(200).body(value.into())?) + } + + let result = match info.handler { + ApiHandler::AsyncHttp(handler) => { + let params = parse_query_parameters(info.parameters, "", &parts, &uri_param)?; + (handler)(parts, req_body, params, info, Box::new(rpcenv)).await + } + ApiHandler::Sync(handler) => { + let params = + get_request_parameters(info.parameters, parts, req_body, uri_param).await?; + (handler)(params, info, &mut rpcenv).and_then(|v| to_json_response(v, &rpcenv)) + } + ApiHandler::Async(handler) => { + let params = + get_request_parameters(info.parameters, parts, req_body, uri_param).await?; + (handler)(params, info, &mut rpcenv) + .await + .and_then(|v| to_json_response(v, &rpcenv)) + } + ApiHandler::StreamingSync(_) => http_bail!( + INTERNAL_SERVER_ERROR, + "old-style streaming calls not supported" + ), + ApiHandler::StreamingAsync(_) => http_bail!( + INTERNAL_SERVER_ERROR, + "old-style streaming calls not supported" + ), + _ => { + bail!("Unknown API handler type"); + } + }; + + let mut resp = match result { + Ok(resp) => resp, + Err(err) => { + if let Some(httperr) = err.downcast_ref::() { + if httperr.code == StatusCode::UNAUTHORIZED { + tokio::time::sleep_until(Instant::from_std(delay_unauth_time())).await; + } + } + return Err(err); + } + }; + + let resp = match compression { + Some(CompressionMethod::Deflate) => { + resp.headers_mut().insert( + header::CONTENT_ENCODING, + CompressionMethod::Deflate.content_encoding(), + ); + resp.map(|body| { + Body::wrap_stream(DeflateEncoder::with_quality( + TryStreamExt::map_err(body, |err| { + proxmox_lang::io_format_err!("error during compression: {}", err) + }), + Level::Default, + )) + }) + } + None => resp, + }; + + if info.reload_timezone { + unsafe { + tzset(); + } + } + + Ok(resp) +} + fn extension_to_content_type(filename: &Path) -> (&'static str, bool) { if let Some(ext) = filename.extension().and_then(|osstr| osstr.to_str()) { return match ext { @@ -575,9 +677,13 @@ async fn handle_static_file_download( filename: PathBuf, compression: Option, ) -> Result, Error> { - let metadata = tokio::fs::metadata(filename.clone()) - .map_err(|err| http_err!(BAD_REQUEST, "File access problems: {}", err)) - .await?; + let metadata = match tokio::fs::metadata(filename.clone()).await { + Ok(metadata) => metadata, + Err(err) if err.kind() == io::ErrorKind::NotFound => { + http_bail!(NOT_FOUND, "no such file: {filename:?}") + } + Err(err) => http_bail!(BAD_REQUEST, "File access problems: {}", err), + }; let (content_type, nocomp) = extension_to_content_type(&filename); let compression = if nocomp { None } else { compression }; @@ -609,9 +715,8 @@ impl ApiConfig { ) -> Result, Error> { let (parts, body) = req.into_parts(); let method = parts.method.clone(); - let (path, components) = normalize_uri_path(parts.uri.path())?; - - let comp_len = components.len(); + let path = normalize_path(parts.uri.path())?; + let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); let query = parts.uri.query().unwrap_or_default(); if path.len() + query.len() > MAX_URI_QUERY_LENGTH { @@ -626,129 +731,320 @@ impl ApiConfig { rpcenv.set_client_ip(Some(*peer)); - let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000); - let access_forbidden_time = - std::time::Instant::now() + std::time::Duration::from_millis(500); + if let Some(handler) = self.find_handler(&components) { + let relative_path_components = &components[handler.prefix.len()..]; + return handler + .handle_request(ApiRequestData { + parts, + body, + peer, + config: &self, + full_path: &path, + relative_path_components, + rpcenv, + }) + .await; + } - if comp_len >= 1 && components[0] == "api2" { - if comp_len >= 2 { - let format = components[1]; + if method != hyper::Method::GET { + http_bail!(BAD_REQUEST, "invalid http method for path"); + } - let formatter: &dyn OutputFormatter = match format { - "json" => JSON_FORMATTER, - "extjs" => EXTJS_FORMATTER, - _ => bail!("Unsupported output format '{}'.", format), - }; - - let mut uri_param = HashMap::new(); - let api_method = self.find_method(&components[2..], method.clone(), &mut uri_param); - - let mut auth_required = true; - if let Some(api_method) = api_method { - if let Permission::World = *api_method.access.permission { - auth_required = false; // no auth for endpoints with World permission - } + if components.is_empty() { + match self.check_auth(&parts.headers, &method).await { + Ok((auth_id, _user_info)) => { + rpcenv.set_auth_id(Some(auth_id)); + return Ok(self.get_index(rpcenv, parts).await); } - - let mut user_info: Box = - Box::new(EmptyUserInformation {}); - - if auth_required { - match self.check_auth(&parts.headers, &method).await { - Ok((authid, info)) => { - rpcenv.set_auth_id(Some(authid)); - user_info = info; - } - Err(auth_err) => { - let err = match auth_err { - AuthError::Generic(err) => err, - AuthError::NoData => { - format_err!("no authentication credentials provided.") - } - }; - // fixme: log Username?? - rpcenv.log_failed_auth(None, &err.to_string()); - - // always delay unauthorized calls by 3 seconds (from start of request) - let err = http_err!(UNAUTHORIZED, "authentication failed - {}", err); - tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await; - return Ok(formatter.format_error(err)); - } - } - } - - match api_method { - None => { - let err = http_err!(NOT_FOUND, "Path '{}' not found.", path); - return Ok(formatter.format_error(err)); - } - Some(api_method) => { - let auth_id = rpcenv.get_auth_id(); - let user_info = user_info; - - if !check_api_permission( - api_method.access.permission, - auth_id.as_deref(), - &uri_param, - user_info.as_ref(), - ) { - let err = http_err!(FORBIDDEN, "permission check failed"); - tokio::time::sleep_until(Instant::from_std(access_forbidden_time)) - .await; - return Ok(formatter.format_error(err)); - } - - let result = - if api_method.protected && env_type == RpcEnvironmentType::PUBLIC { - proxy_protected_request(api_method, parts, body, peer).await - } else { - handle_api_request( - rpcenv, api_method, formatter, parts, body, uri_param, - ) - .await - }; - - let mut response = match result { - Ok(resp) => resp, - Err(err) => formatter.format_error(err), - }; - - if let Some(auth_id) = auth_id { - response - .extensions_mut() - .insert(AuthStringExtension(auth_id)); - } - - return Ok(response); - } + Err(AuthError::Generic(_)) => { + tokio::time::sleep_until(Instant::from_std(delay_unauth_time())).await; } + Err(AuthError::NoData) => {} } + return Ok(self.get_index(rpcenv, parts).await); } else { - // not Auth required for accessing files! + let filename = self.find_alias(&components); + let compression = extract_compression_method(&parts.headers); + return handle_static_file_download(filename, compression).await; + } + } +} - if method != hyper::Method::GET { - http_bail!(BAD_REQUEST, "invalid http method for path"); - } +pub(crate) struct Handler { + pub prefix: &'static [&'static str], + action: Action, +} - if comp_len == 0 { - match self.check_auth(&parts.headers, &method).await { - Ok((auth_id, _user_info)) => { - rpcenv.set_auth_id(Some(auth_id)); - return Ok(self.get_index(rpcenv, parts).await); - } - Err(AuthError::Generic(_)) => { - tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await; - } - Err(AuthError::NoData) => {} - } - return Ok(self.get_index(rpcenv, parts).await); - } else { - let filename = self.find_alias(&components); - let compression = extract_compression_method(&parts.headers); - return handle_static_file_download(filename, compression).await; +impl Handler { + async fn handle_request(&self, data: ApiRequestData<'_>) -> Result, Error> { + self.action.handle_request(data).await + } + + pub(crate) fn default_api2_handler(router: &'static proxmox_router::Router) -> Self { + Self::formatted_router(&["api2"], router) + } + + pub(crate) fn formatted_router( + prefix: &'static [&'static str], + router: &'static proxmox_router::Router, + ) -> Self { + Self { + prefix, + action: Action::Formatted(Formatted { router }), + } + } + + pub(crate) fn unformatted_router( + prefix: &'static [&'static str], + router: &'static proxmox_router::Router, + ) -> Self { + Self { + prefix, + action: Action::Unformatted(Unformatted { router }), + } + } +} + +pub(crate) enum Action { + Formatted(Formatted), + Unformatted(Unformatted), +} + +impl Action { + async fn handle_request(&self, data: ApiRequestData<'_>) -> Result, Error> { + match self { + Action::Formatted(a) => a.handle_request(data).await, + Action::Unformatted(a) => a.handle_request(data).await, + } + } +} + +pub struct ApiRequestData<'a> { + parts: Parts, + body: Body, + peer: &'a std::net::SocketAddr, + config: &'a ApiConfig, + full_path: &'a str, + relative_path_components: &'a [&'a str], + rpcenv: RestEnvironment, +} + +pub(crate) struct Formatted { + router: &'static proxmox_router::Router, +} + +impl Formatted { + pub async fn handle_request( + &self, + ApiRequestData { + parts, + body, + peer, + config, + full_path, + relative_path_components, + mut rpcenv, + }: ApiRequestData<'_>, + ) -> Result, Error> { + if relative_path_components.is_empty() { + http_bail!(NOT_FOUND, "invalid api path '{}'", full_path); + } + + let format = relative_path_components[0]; + + let formatter: &dyn OutputFormatter = match format { + "json" => JSON_FORMATTER, + "extjs" => EXTJS_FORMATTER, + _ => bail!("Unsupported output format '{}'.", format), + }; + + let mut uri_param = HashMap::new(); + let api_method = self.router.find_method( + &relative_path_components[1..], + parts.method.clone(), + &mut uri_param, + ); + + let mut auth_required = true; + if let Some(api_method) = api_method { + if let Permission::World = *api_method.access.permission { + auth_required = false; // no auth for endpoints with World permission } } - Err(http_err!(NOT_FOUND, "Path '{}' not found.", path)) + let mut user_info: Box = + Box::new(EmptyUserInformation {}); + + if auth_required { + match config.check_auth(&parts.headers, &parts.method).await { + Ok((authid, info)) => { + rpcenv.set_auth_id(Some(authid)); + user_info = info; + } + Err(auth_err) => { + let err = match auth_err { + AuthError::Generic(err) => err, + AuthError::NoData => { + format_err!("no authentication credentials provided.") + } + }; + // fixme: log Username?? + rpcenv.log_failed_auth(None, &err.to_string()); + + // always delay unauthorized calls by 3 seconds (from start of request) + let err = http_err!(UNAUTHORIZED, "authentication failed - {}", err); + tokio::time::sleep_until(Instant::from_std(delay_unauth_time())).await; + return Ok(formatter.format_error(err)); + } + } + } + + match api_method { + None => { + let err = http_err!(NOT_FOUND, "Path '{}' not found.", full_path); + return Ok(formatter.format_error(err)); + } + Some(api_method) => { + let auth_id = rpcenv.get_auth_id(); + let user_info = user_info; + + if !check_api_permission( + api_method.access.permission, + auth_id.as_deref(), + &uri_param, + user_info.as_ref(), + ) { + let err = http_err!(FORBIDDEN, "permission check failed"); + tokio::time::sleep_until(Instant::from_std(access_forbidden_time())).await; + return Ok(formatter.format_error(err)); + } + + let result = if api_method.protected + && rpcenv.env_type == RpcEnvironmentType::PUBLIC + { + proxy_protected_request(api_method, parts, body, &peer).await + } else { + handle_api_request(rpcenv, api_method, formatter, parts, body, uri_param).await + }; + + let mut response = match result { + Ok(resp) => resp, + Err(err) => formatter.format_error(err), + }; + + if let Some(auth_id) = auth_id { + response + .extensions_mut() + .insert(AuthStringExtension(auth_id)); + } + + return Ok(response); + } + } + } +} + +pub(crate) struct Unformatted { + router: &'static proxmox_router::Router, +} + +impl Unformatted { + pub async fn handle_request( + &self, + ApiRequestData { + parts, + body, + peer, + config, + full_path, + relative_path_components, + mut rpcenv, + }: ApiRequestData<'_>, + ) -> Result, Error> { + if relative_path_components.is_empty() { + http_bail!(NOT_FOUND, "invalid api path '{}'", full_path); + } + + let mut uri_param = HashMap::new(); + let api_method = self.router.find_method( + relative_path_components, + parts.method.clone(), + &mut uri_param, + ); + + let mut auth_required = true; + if let Some(api_method) = api_method { + if let Permission::World = *api_method.access.permission { + auth_required = false; // no auth for endpoints with World permission + } + } + + let user_info: Box; + + if auth_required { + match config.check_auth(&parts.headers, &parts.method).await { + Ok((authid, info)) => { + rpcenv.set_auth_id(Some(authid)); + user_info = info; + } + Err(auth_err) => { + let err = match auth_err { + AuthError::Generic(err) => err, + AuthError::NoData => { + format_err!("no authentication credentials provided.") + } + }; + // fixme: log Username?? + rpcenv.log_failed_auth(None, &err.to_string()); + + // always delay unauthorized calls by 3 seconds (from start of request) + let err = http_err!(UNAUTHORIZED, "authentication failed - {}", err); + tokio::time::sleep_until(Instant::from_std(delay_unauth_time())).await; + return Err(err); + } + } + } else { + user_info = Box::new(EmptyUserInformation {}); + } + + match api_method { + None => http_bail!(NOT_FOUND, "Path '{}' not found.", full_path), + Some(api_method) => { + let auth_id = rpcenv.get_auth_id(); + let user_info = user_info; + + if !check_api_permission( + api_method.access.permission, + auth_id.as_deref(), + &uri_param, + user_info.as_ref(), + ) { + let err = http_err!(FORBIDDEN, "permission check failed"); + tokio::time::sleep_until(Instant::from_std(access_forbidden_time())).await; + return Err(err); + } + + let result = if api_method.protected + && rpcenv.env_type == RpcEnvironmentType::PUBLIC + { + proxy_protected_request(api_method, parts, body, &peer).await + } else { + handle_unformatted_api_request(rpcenv, api_method, parts, body, uri_param).await + }; + + let mut response = match result { + Ok(resp) => resp, + Err(err) => crate::formatter::error_to_response(err), + }; + + if let Some(auth_id) = auth_id { + response + .extensions_mut() + .insert(AuthStringExtension(auth_id)); + } + + return Ok(response); + } + } } }