diff --git a/proxmox-rest-server/src/daemon.rs b/proxmox-rest-server/src/daemon.rs index 5401e30c..789ba47c 100644 --- a/proxmox-rest-server/src/daemon.rs +++ b/proxmox-rest-server/src/daemon.rs @@ -1,4 +1,4 @@ -//! Helpers for daemons/services. +//! Helpers to implement restartable daemons/services. use std::ffi::CString; use std::future::Future; @@ -351,6 +351,7 @@ extern "C" { fn sd_notify(unset_environment: c_int, state: *const c_char) -> c_int; } +/// Systemd sercice startup states (see: ``man sd_notify``) pub enum SystemdNotify { Ready, Reloading, @@ -359,6 +360,7 @@ pub enum SystemdNotify { MainPid(nix::unistd::Pid), } +/// Tells systemd the startup state of the service (see: ``man sd_notify``) pub fn systemd_notify(state: SystemdNotify) -> Result<(), Error> { let message = match state { SystemdNotify::Ready => CString::new("READY=1"), diff --git a/proxmox-rest-server/src/formatter.rs b/proxmox-rest-server/src/formatter.rs index 8818e671..7f41b382 100644 --- a/proxmox-rest-server/src/formatter.rs +++ b/proxmox-rest-server/src/formatter.rs @@ -1,3 +1,5 @@ +//! Helpers to format response data + use anyhow::{Error}; use serde_json::{json, Value}; @@ -7,25 +9,28 @@ use hyper::header; use proxmox::api::{HttpError, RpcEnvironment}; /// Extension to set error message for server side logging -pub struct ErrorMessageExtension(pub String); +pub(crate) struct ErrorMessageExtension(pub String); -pub struct OutputFormatter { +/// Methods to format data and errors +pub trait OutputFormatter: Send + Sync { + /// Transform json data into a http response + fn format_data(&self, data: Value, rpcenv: &dyn RpcEnvironment) -> Response; - pub format_data: fn(data: Value, rpcenv: &dyn RpcEnvironment) -> Response, + /// Transform errors into a http response + fn format_error(&self, err: Error) -> Response; - pub format_error: fn(err: Error) -> Response, + /// Transform a [Result] into a http response + fn format_result(&self, result: Result, rpcenv: &dyn RpcEnvironment) -> Response { + match result { + Ok(data) => self.format_data(data, rpcenv), + Err(err) => self.format_error(err), + } + } } static JSON_CONTENT_TYPE: &str = "application/json;charset=UTF-8"; -pub fn json_response(result: Result) -> Response { - match result { - Ok(data) => json_data_response(data), - Err(err) => json_error_response(err), - } -} - -pub fn json_data_response(data: Value) -> Response { +fn json_data_response(data: Value) -> Response { let json_str = data.to_string(); @@ -51,76 +56,101 @@ fn add_result_attributes(result: &mut Value, rpcenv: &dyn RpcEnvironment) } } -fn json_format_data(data: Value, rpcenv: &dyn RpcEnvironment) -> Response { - let mut result = json!({ - "data": data - }); +struct JsonFormatter(); - add_result_attributes(&mut result, rpcenv); +/// Format data as ``application/json`` +/// +/// Errors generates a BAD_REQUEST containing the error +/// message as string. +pub static JSON_FORMATTER: &'static dyn OutputFormatter = &JsonFormatter(); - json_data_response(result) +impl OutputFormatter for JsonFormatter { + + fn format_data(&self, data: Value, rpcenv: &dyn RpcEnvironment) -> Response { + + let mut result = json!({ + "data": data + }); + + add_result_attributes(&mut result, rpcenv); + + json_data_response(result) + } + + 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 + } } -pub fn json_error_response(err: Error) -> Response { +/// Format data as ExtJS compatible ``application/json`` +/// +/// The returned json object contains the following properties: +/// +/// * ``success``: boolean attribute indicating the success. +/// +/// * ``data``: The result data (on success) +/// +/// * ``message``: The error message (on failure) +/// +/// * ``errors``: detailed list of errors (if available) +/// +/// Any result attributes set on ``rpcenv`` are also added to the object. +/// +/// Please note that errors return status code OK, but setting success +/// to false. +pub static EXTJS_FORMATTER: &'static dyn OutputFormatter = &ExtJsFormatter(); - 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 - }; +struct ExtJsFormatter(); - response.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static(JSON_CONTENT_TYPE)); +impl OutputFormatter for ExtJsFormatter { - response.extensions_mut().insert(ErrorMessageExtension(err.to_string())); + fn format_data(&self, data: Value, rpcenv: &dyn RpcEnvironment) -> Response { - response + let mut result = json!({ + "data": data, + "success": true + }); + + add_result_attributes(&mut result, rpcenv); + + json_data_response(result) + } + + fn format_error(&self, err: Error) -> Response { + + let mut errors = vec![]; + + let message = err.to_string(); + errors.push(&message); + + let result = json!({ + "message": message, + "errors": errors, + "success": false + }); + + let mut response = json_data_response(result); + + response.extensions_mut().insert(ErrorMessageExtension(message)); + + response + } } - -pub static JSON_FORMATTER: OutputFormatter = OutputFormatter { - format_data: json_format_data, - format_error: json_error_response, -}; - -fn extjs_format_data(data: Value, rpcenv: &dyn RpcEnvironment) -> Response { - - let mut result = json!({ - "data": data, - "success": true - }); - - add_result_attributes(&mut result, rpcenv); - - json_data_response(result) -} - -fn extjs_format_error(err: Error) -> Response { - - let mut errors = vec![]; - - let message = err.to_string(); - errors.push(&message); - - let result = json!({ - "message": message, - "errors": errors, - "success": false - }); - - let mut response = json_data_response(result); - - response.extensions_mut().insert(ErrorMessageExtension(message)); - - response -} - -pub static EXTJS_FORMATTER: OutputFormatter = OutputFormatter { - format_data: extjs_format_data, - format_error: extjs_format_error, -}; diff --git a/proxmox-rest-server/src/h2service.rs b/proxmox-rest-server/src/h2service.rs index fba9714c..f7c7b978 100644 --- a/proxmox-rest-server/src/h2service.rs +++ b/proxmox-rest-server/src/h2service.rs @@ -51,12 +51,12 @@ impl H2Service { let mut uri_param = HashMap::new(); - let formatter = &JSON_FORMATTER; + let formatter = JSON_FORMATTER; match self.router.find_method(&components, method, &mut uri_param) { None => { let err = http_err!(NOT_FOUND, "Path '{}' not found.", path); - future::ok((formatter.format_error)(err)).boxed() + future::ok(formatter.format_error(err)).boxed() } Some(api_method) => { crate::rest::handle_api_request( diff --git a/proxmox-rest-server/src/lib.rs b/proxmox-rest-server/src/lib.rs index 5ba72ce0..697e2242 100644 --- a/proxmox-rest-server/src/lib.rs +++ b/proxmox-rest-server/src/lib.rs @@ -12,6 +12,7 @@ mod compression; pub use compression::*; pub mod daemon; + pub mod formatter; mod environment; diff --git a/proxmox-rest-server/src/rest.rs b/proxmox-rest-server/src/rest.rs index 929e4043..ba2edf11 100644 --- a/proxmox-rest-server/src/rest.rs +++ b/proxmox-rest-server/src/rest.rs @@ -391,7 +391,7 @@ async fn proxy_protected_request( pub(crate) async fn handle_api_request( mut rpcenv: Env, info: &'static ApiMethod, - formatter: &'static OutputFormatter, + formatter: &'static dyn OutputFormatter, parts: Parts, req_body: Body, uri_param: HashMap, @@ -407,14 +407,14 @@ pub(crate) async fn handle_api_request { let params = get_request_parameters(info.parameters, parts, req_body, uri_param).await?; - (handler)(params, info, &mut rpcenv).map(|data| (formatter.format_data)(data, &rpcenv)) + (handler)(params, info, &mut rpcenv).map(|data| formatter.format_data(data, &rpcenv)) } ApiHandler::Async(handler) => { let params = get_request_parameters(info.parameters, parts, req_body, uri_param).await?; (handler)(params, info, &mut rpcenv) .await - .map(|data| (formatter.format_data)(data, &rpcenv)) + .map(|data| formatter.format_data(data, &rpcenv)) } }; @@ -426,7 +426,7 @@ pub(crate) async fn handle_api_request= 2 { let format = components[1]; - let formatter = match format { - "json" => &JSON_FORMATTER, - "extjs" => &EXTJS_FORMATTER, + let formatter: &dyn OutputFormatter = match format { + "json" => JSON_FORMATTER, + "extjs" => EXTJS_FORMATTER, _ => bail!("Unsupported output format '{}'.", format), }; @@ -664,7 +664,7 @@ async fn handle_request( // 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)); + return Ok(formatter.format_error(err)); } } } @@ -672,7 +672,7 @@ async fn handle_request( match api_method { None => { let err = http_err!(NOT_FOUND, "Path '{}' not found.", path); - return Ok((formatter.format_error)(err)); + return Ok(formatter.format_error(err)); } Some(api_method) => { let auth_id = rpcenv.get_auth_id(); @@ -686,7 +686,7 @@ async fn handle_request( ) { 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)); + return Ok(formatter.format_error(err)); } let result = if api_method.protected && env_type == RpcEnvironmentType::PUBLIC { @@ -698,7 +698,7 @@ async fn handle_request( let mut response = match result { Ok(resp) => resp, - Err(err) => (formatter.format_error)(err), + Err(err) => formatter.format_error(err), }; if let Some(auth_id) = auth_id {