diff --git a/src/api2.rs b/src/api2.rs index 315744e5..8053a5ce 100644 --- a/src/api2.rs +++ b/src/api2.rs @@ -1,17 +1,18 @@ -pub mod types; -pub mod config; +mod access; pub mod admin; pub mod backup; -pub mod reader; +pub mod config; pub mod node; -pub mod version; +pub mod reader; mod subscription; -mod access; +pub mod types; +pub mod version; -use crate::api_schema::router::*; +use proxmox::api::list_subdirs_api_method; +use proxmox::api::router::SubdirMap; +use proxmox::api::Router; -const NODES_ROUTER: Router = Router::new() - .match_all("node", &node::ROUTER); +const NODES_ROUTER: Router = Router::new().match_all("node", &node::ROUTER); pub const SUBDIRS: SubdirMap = &[ ("access", &access::ROUTER), diff --git a/src/api2/admin.rs b/src/api2/admin.rs index a2c052e4..e782d68c 100644 --- a/src/api2/admin.rs +++ b/src/api2/admin.rs @@ -1,4 +1,5 @@ -use crate::api_schema::router::*; +use proxmox::api::router::{Router, SubdirMap}; +use proxmox::api::list_subdirs_api_method; pub mod datastore; diff --git a/src/api2/config.rs b/src/api2/config.rs index a2c052e4..e782d68c 100644 --- a/src/api2/config.rs +++ b/src/api2/config.rs @@ -1,4 +1,5 @@ -use crate::api_schema::router::*; +use proxmox::api::router::{Router, SubdirMap}; +use proxmox::api::list_subdirs_api_method; pub mod datastore; diff --git a/src/api2/node.rs b/src/api2/node.rs index fad7a739..778deaf1 100644 --- a/src/api2/node.rs +++ b/src/api2/node.rs @@ -1,4 +1,5 @@ -use crate::api_schema::router::*; +use proxmox::api::router::{Router, SubdirMap}; +use proxmox::api::list_subdirs_api_method; mod tasks; mod time; diff --git a/src/api_schema.rs b/src/api_schema.rs index 6e298215..a35cf1e3 100644 --- a/src/api_schema.rs +++ b/src/api_schema.rs @@ -8,16 +8,31 @@ //! hierarchy of API entries, and provides ways to find an API //! definition by path. -#[macro_use] -mod schema; -pub use schema::*; - -pub mod rpc_environment; -pub mod api_handler; -#[macro_use] -pub mod router; - //pub mod registry; pub mod config; pub mod format; +/* + * -------------------------------------------------------------------------------------------- + * Everything below is a compatibility layer to support building the current code until api2.rs + * and the api2/ directory have been updated to the proxmox::api crate: + * -------------------------------------------------------------------------------------------- + */ + +pub use proxmox::api::schema::*; +pub use proxmox::api::*; + +pub use proxmox::api::ApiFuture as BoxFut; + +pub mod api_handler { + pub use super::{ApiAsyncHandlerFn, ApiHandler, ApiHandlerFn, BoxFut}; +} + +pub mod router { + pub use super::{ApiHandler, ApiMethod, HttpError, RpcEnvironment, RpcEnvironmentType}; + pub use proxmox::api::router::*; +} + +pub mod schema { + pub use proxmox::api::schema::*; +} diff --git a/src/api_schema/api_handler.rs b/src/api_schema/api_handler.rs deleted file mode 100644 index c818bcd1..00000000 --- a/src/api_schema/api_handler.rs +++ /dev/null @@ -1,26 +0,0 @@ -use failure::Error; -use serde_json::Value; - -use hyper::{Body, Response}; -use hyper::rt::Future; -use hyper::http::request::Parts; - -use super::rpc_environment::RpcEnvironment; -use super::router::ApiMethod; - -pub type BoxFut = Box, failure::Error>> + Send>; - -pub type ApiHandlerFn = &'static ( - dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result - + Send + Sync + 'static -); - -pub type ApiAsyncHandlerFn = &'static ( - dyn Fn(Parts, Body, Value, &'static ApiMethod, Box) -> Result - + Send + Sync + 'static -); - -pub enum ApiHandler { - Sync(ApiHandlerFn), - Async(ApiAsyncHandlerFn), -} diff --git a/src/api_schema/router.rs b/src/api_schema/router.rs deleted file mode 100644 index 1fd9eac4..00000000 --- a/src/api_schema/router.rs +++ /dev/null @@ -1,261 +0,0 @@ -use failure::*; - -use serde_json::Value; -use std::collections::HashMap; -use std::fmt; - -use hyper::{Method, StatusCode}; -//use hyper::http::request::Parts; - -use super::schema::*; -pub use super::rpc_environment::*; -pub use super::api_handler::*; - - -#[derive(Debug, Fail)] -pub struct HttpError { - pub code: StatusCode, - pub message: String, -} - -impl HttpError { - pub fn new(code: StatusCode, message: String) -> Self { - HttpError { code, message } - } -} - -impl fmt::Display for HttpError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.message) - } -} - -#[macro_export] -macro_rules! http_err { - ($status:ident, $msg:expr) => {{ - Error::from(HttpError::new(StatusCode::$status, $msg)) - }} -} - -/// This struct defines synchronous API call which returns the restulkt as json `Value` -pub struct ApiMethod { - /// The protected flag indicates that the provides function should be forwarded - /// to the deaemon running in priviledged mode. - pub protected: bool, - /// This flag indicates that the provided method may change the local timezone, so the server - /// should do a tzset afterwards - pub reload_timezone: bool, - /// Parameter type Schema - pub parameters: &'static ObjectSchema, - /// Return type Schema - pub returns: &'static Schema, - /// Handler function - pub handler: &'static ApiHandler, -} - -impl std::fmt::Debug for ApiMethod { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "ApiMethod {{ ")?; - write!(f, " parameters: {:?}", self.parameters)?; - write!(f, " returns: {:?}", self.returns)?; - write!(f, " handler: {:p}", &self.handler)?; - write!(f, "}}") - } -} - -const NULL_SCHEMA: Schema = Schema::Null; - -fn dummy_handler_fn(_arg: Value, _method: &ApiMethod, _env: &mut dyn RpcEnvironment) -> Result { - // do nothing - Ok(Value::Null) -} - -const DUMMY_HANDLER: ApiHandler = ApiHandler::Sync(&dummy_handler_fn); - -impl ApiMethod { - - pub const fn new(handler: &'static ApiHandler, parameters: &'static ObjectSchema) -> Self { - Self { - parameters, - handler, - returns: &NULL_SCHEMA, - protected: false, - reload_timezone: false, - } - } - - pub const fn new_dummy(parameters: &'static ObjectSchema) -> Self { - Self { - parameters, - handler: &DUMMY_HANDLER, - returns: &NULL_SCHEMA, - protected: false, - reload_timezone: false, - } - } - - pub const fn returns(mut self, schema: &'static Schema) -> Self { - - self.returns = schema; - - self - } - - pub const fn protected(mut self, protected: bool) -> Self { - - self.protected = protected; - - self - } - - pub const fn reload_timezone(mut self, reload_timezone: bool) -> Self { - - self.reload_timezone = reload_timezone; - - self - } -} - -pub type SubdirMap = &'static [(&'static str, &'static Router)]; - -pub enum SubRoute { - //Hash(HashMap), - Map(SubdirMap), - MatchAll { router: &'static Router, param_name: &'static str }, -} - -/// Macro to create an ApiMethod to list entries from SubdirMap -#[macro_export] -macro_rules! list_subdirs_api_method { - ($map:expr) => { - ApiMethod::new( - &ApiHandler::Sync( & |_, _, _| { - let index = serde_json::json!( - $map.iter().map(|s| serde_json::json!({ "subdir": s.0})) - .collect::>() - ); - Ok(index) - }), - &crate::api_schema::ObjectSchema::new("Directory index.", &[]).additional_properties(true) - ) - } -} - -pub struct Router { - pub get: Option<&'static ApiMethod>, - pub put: Option<&'static ApiMethod>, - pub post: Option<&'static ApiMethod>, - pub delete: Option<&'static ApiMethod>, - pub subroute: Option, -} - -impl Router { - - pub const fn new() -> Self { - Self { - get: None, - put: None, - post: None, - delete: None, - subroute: None, - } - } - - pub const fn subdirs(mut self, map: SubdirMap) -> Self { - self.subroute = Some(SubRoute::Map(map)); - self - } - - pub const fn match_all(mut self, param_name: &'static str, router: &'static Router) -> Self { - self.subroute = Some(SubRoute::MatchAll { router, param_name }); - self - } - - pub const fn get(mut self, m: &'static ApiMethod) -> Self { - self.get = Some(m); - self - } - - pub const fn put(mut self, m: &'static ApiMethod) -> Self { - self.put = Some(m); - self - } - - pub const fn post(mut self, m: &'static ApiMethod) -> Self { - self.post = Some(m); - self - } - - /// Same as post, buth async (fixme: expect Async) - pub const fn upload(mut self, m: &'static ApiMethod) -> Self { - self.post = Some(m); - self - } - - /// Same as get, but async (fixme: expect Async) - pub const fn download(mut self, m: &'static ApiMethod) -> Self { - self.get = Some(m); - self - } - - /// Same as get, but async (fixme: expect Async) - pub const fn upgrade(mut self, m: &'static ApiMethod) -> Self { - self.get = Some(m); - self - } - - pub const fn delete(mut self, m: &'static ApiMethod) -> Self { - self.delete = Some(m); - self - } - - pub fn find_route(&self, components: &[&str], uri_param: &mut HashMap) -> Option<&Router> { - - if components.is_empty() { return Some(self); }; - - let (dir, rest) = (components[0], &components[1..]); - - match self.subroute { - None => {}, - Some(SubRoute::Map(dirmap)) => { - if let Ok(ind) = dirmap.binary_search_by_key(&dir, |(name, _)| name) { - let (_name, router) = dirmap[ind]; - //println!("FOUND SUBDIR {}", dir); - return router.find_route(rest, uri_param); - } - } - Some(SubRoute::MatchAll { router, param_name }) => { - //println!("URI PARAM {} = {}", param_name, dir); // fixme: store somewhere - uri_param.insert(param_name.to_owned(), dir.into()); - return router.find_route(rest, uri_param); - }, - } - - None - } - - pub fn find_method( - &self, - components: &[&str], - method: Method, - uri_param: &mut HashMap - ) -> Option<&ApiMethod> { - - if let Some(info) = self.find_route(components, uri_param) { - return match method { - Method::GET => info.get, - Method::PUT => info.put, - Method::POST => info.post, - Method::DELETE => info.delete, - _ => None, - }; - } - None - } -} - -impl Default for Router { - fn default() -> Self { - Self::new() - } -} diff --git a/src/api_schema/rpc_environment.rs b/src/api_schema/rpc_environment.rs deleted file mode 100644 index 1b02756c..00000000 --- a/src/api_schema/rpc_environment.rs +++ /dev/null @@ -1,37 +0,0 @@ -use serde_json::Value; - -/// Abstract Interface for API methods to interact with the environment -pub trait RpcEnvironment: std::any::Any + crate::tools::AsAny + Send { - - /// Use this to pass additional result data. It is up to the environment - /// how the data is used. - fn set_result_attrib(&mut self, name: &str, value: Value); - - /// Query additional result data. - fn get_result_attrib(&self, name: &str) -> Option<&Value>; - - /// The environment type - fn env_type(&self) -> RpcEnvironmentType; - - /// Set user name - fn set_user(&mut self, user: Option); - - /// Get user name - fn get_user(&self) -> Option; -} - - -/// Environment Type -/// -/// We use this to enumerate the different environment types. Some methods -/// needs to do different things when started from the command line interface, -/// or when executed from a privileged server running as root. -#[derive(PartialEq, Copy, Clone)] -pub enum RpcEnvironmentType { - /// Command started from command line - CLI, - /// Access from public accessible server - PUBLIC, - /// Access from privileged server (run as root) - PRIVILEGED, -} diff --git a/src/api_schema/schema.rs b/src/api_schema/schema.rs deleted file mode 100644 index f60f6d39..00000000 --- a/src/api_schema/schema.rs +++ /dev/null @@ -1,1018 +0,0 @@ -use failure::*; -use serde_json::{json, Value}; -use url::form_urlencoded; -use std::fmt; - -#[derive(Default, Debug, Fail)] -pub struct ParameterError { - error_list: Vec, -} - -/// Error type for schema validation -/// -/// The validation functions may produce several error message, -/// i.e. when validation objects, it can produce one message for each -/// erroneous object property. - -// fixme: record parameter names, to make it usefull to display errord -// on HTML forms. -impl ParameterError { - - pub fn new() -> Self { - Self { error_list: Vec::new() } - } - - pub fn push(&mut self, value: Error) { - self.error_list.push(value); - } - - pub fn len(&self) -> usize { - self.error_list.len() - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl fmt::Display for ParameterError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - - let mut msg = String::new(); - - if !self.is_empty() { - msg.push_str("parameter verification errors\n\n"); - } - - msg.push_str(&self.error_list.iter().fold(String::from(""), |acc, item| { - acc + &item.to_string() + "\n" - })); - - write!(f, "{}", msg) - } -} - -#[derive(Debug)] -pub struct BooleanSchema { - pub description: &'static str, - pub default: Option, -} - -impl BooleanSchema { - - pub const fn new(description: &'static str) -> Self { - BooleanSchema { - description, - default: None, - } - } - - pub const fn default(mut self, default: bool) -> Self { - self.default = Some(default); - self - } - - pub const fn schema(self) -> Schema { - Schema::Boolean(self) - } -} - -#[derive(Debug)] -pub struct IntegerSchema { - pub description: &'static str, - pub minimum: Option, - pub maximum: Option, - pub default: Option, -} - -impl IntegerSchema { - - pub const fn new(description: &'static str) -> Self { - IntegerSchema { - description, - default: None, - minimum: None, - maximum: None, - } - } - - pub const fn default(mut self, default: isize) -> Self { - self.default = Some(default); - self - } - - pub const fn minimum(mut self, minimum: isize) -> Self { - self.minimum = Some(minimum); - self - } - - pub const fn maximum(mut self, maximium: isize) -> Self { - self.maximum = Some(maximium); - self - } - - pub const fn schema(self) -> Schema { - Schema::Integer(self) - } - - fn check_constraints(&self, value: isize) -> Result<(), Error> { - - if let Some(minimum) = self.minimum { - if value < minimum { - bail!("value must have a minimum value of {} (got {})", minimum, value); - } - } - - if let Some(maximum) = self.maximum { - if value > maximum { - bail!("value must have a maximum value of {} (got {})", maximum, value); - } - } - - Ok(()) - } -} - -/// Helper to represent const regular expressions -/// -/// This is mostly a workaround, unless we can create const_fn Regex. -pub struct ConstRegexPattern { - pub regex_string: &'static str, - pub regex_obj: fn() -> &'static regex::Regex, -} - -impl std::fmt::Debug for ConstRegexPattern { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.regex_string) - } -} - -/// Macro to generate a ConstRegexPattern -#[macro_export] -macro_rules! const_regex { - () => {}; - ($(#[$attr:meta])* pub ($($vis:tt)+) $name:ident = $regex:expr; $($rest:tt)*) => { - const_regex! { (pub ($($vis)+)) $(#[$attr])* $name = $regex; $($rest)* } - }; - ($(#[$attr:meta])* pub $name:ident = $regex:expr; $($rest:tt)*) => { - const_regex! { (pub) $(#[$attr])* $name = $regex; $($rest)* } - }; - ($(#[$attr:meta])* $name:ident = $regex:expr; $($rest:tt)*) => { - const_regex! { () $(#[$attr])* $name = $regex; $($rest)* } - }; - ( - ($($pub:tt)*) $(#[$attr:meta])* $name:ident = $regex:expr; - $($rest:tt)* - ) => { - $(#[$attr])* $($pub)* const $name: ConstRegexPattern = ConstRegexPattern { - regex_string: $regex, - regex_obj: (|| -> &'static regex::Regex { - lazy_static::lazy_static! { - static ref SCHEMA: regex::Regex = regex::Regex::new($regex).unwrap(); - } - &SCHEMA - }) - }; - - const_regex! { $($rest)* } - }; -} - -#[derive(Debug)] -pub struct StringSchema { - pub description: &'static str, - pub default: Option<&'static str>, - pub min_length: Option, - pub max_length: Option, - pub format: Option<&'static ApiStringFormat>, -} - -impl StringSchema { - - pub const fn new(description: &'static str) -> Self { - StringSchema { - description, - default: None, - min_length: None, - max_length: None, - format: None, - } - } - - pub const fn default(mut self, text: &'static str) -> Self { - self.default = Some(text); - self - } - - pub const fn format(mut self, format: &'static ApiStringFormat) -> Self { - self.format = Some(format); - self - } - - pub const fn min_length(mut self, min_length: usize) -> Self { - self.min_length = Some(min_length); - self - } - - pub const fn max_length(mut self, max_length: usize) -> Self { - self.max_length = Some(max_length); - self - } - - pub const fn schema(self) -> Schema { - Schema::String(self) - } - - fn check_length(&self, length: usize) -> Result<(), Error> { - - if let Some(min_length) = self.min_length { - if length < min_length { - bail!("value must be at least {} characters long", min_length); - } - } - - if let Some(max_length) = self.max_length { - if length > max_length { - bail!("value may only be {} characters long", max_length); - } - } - - Ok(()) - } - - pub fn check_constraints(&self, value: &str) -> Result<(), Error> { - - self.check_length(value.chars().count())?; - - if let Some(ref format) = self.format { - match format { - ApiStringFormat::Pattern(regex) => { - if !(regex.regex_obj)().is_match(value) { - bail!("value does not match the regex pattern"); - } - } - ApiStringFormat::Enum(stringvec) => { - if stringvec.iter().find(|&e| *e == value) == None { - bail!("value '{}' is not defined in the enumeration.", value); - } - } - ApiStringFormat::Complex(subschema) => { - parse_property_string(value, subschema)?; - } - ApiStringFormat::VerifyFn(verify_fn) => { - verify_fn(value)?; - } - } - } - - Ok(()) - } -} - -#[derive(Debug)] -pub struct ArraySchema { - pub description: &'static str, - pub items: &'static Schema, - pub min_length: Option, - pub max_length: Option, -} - -impl ArraySchema { - - pub const fn new(description: &'static str, item_schema: &'static Schema) -> Self { - ArraySchema { - description, - items: item_schema, - min_length: None, - max_length: None, - } - } - - pub const fn min_length(mut self, min_length: usize) -> Self { - self.min_length = Some(min_length); - self - } - - pub const fn max_length(mut self, max_length: usize) -> Self { - self.max_length = Some(max_length); - self - } - - pub const fn schema(self) -> Schema { - Schema::Array(self) - } - - fn check_length(&self, length: usize) -> Result<(), Error> { - - if let Some(min_length) = self.min_length { - if length < min_length { - bail!("array must contain at least {} elements", min_length); - } - } - - if let Some(max_length) = self.max_length { - if length > max_length { - bail!("array may only contain {} elements", max_length); - } - } - - Ok(()) - } -} - -/// Lookup table to Schema properties -/// -/// Stores a sorted list of (name, optional, schema) tuples: -/// -/// name: The name of the property -/// optional: Set when the property is optional -/// schema: Property type schema -/// -/// NOTE: The list has to be storted by name, because we use -/// a binary search to find items. -/// -/// This is a workaround unless RUST can const_fn Hash::new() -pub type SchemaPropertyMap = &'static [(&'static str, bool, &'static Schema)]; - -#[derive(Debug)] -pub struct ObjectSchema { - pub description: &'static str, - pub additional_properties: bool, - pub properties: SchemaPropertyMap, - pub default_key: Option<&'static str>, -} - -impl ObjectSchema { - - pub const fn new(description: &'static str, properties: SchemaPropertyMap) -> Self { - ObjectSchema { - description, - properties, - additional_properties: false, - default_key: None, - } - } - - pub const fn additional_properties(mut self, additional_properties: bool) -> Self { - self.additional_properties = additional_properties; - self - } - - pub const fn default_key(mut self, key: &'static str) -> Self { - self.default_key = Some(key); - self - } - - pub const fn schema(self) -> Schema { - Schema::Object(self) - } - - pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> { - if let Ok(ind) = self.properties.binary_search_by_key(&key, |(name, _, _)| name) { - let (_name, optional, prop_schema) = self.properties[ind]; - Some((optional, prop_schema)) - } else { - None - } - } -} - -#[derive(Debug)] -pub enum Schema { - Null, - Boolean(BooleanSchema), - Integer(IntegerSchema), - String(StringSchema), - Object(ObjectSchema), - Array(ArraySchema), -} - -pub enum ApiStringFormat { - Enum(&'static [&'static str]), - Pattern(&'static ConstRegexPattern), - Complex(&'static Schema), - VerifyFn(fn(&str) -> Result<(), Error>), -} - -impl std::fmt::Debug for ApiStringFormat { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ApiStringFormat::VerifyFn(fnptr) => { - write!(f, "VerifyFn({:p}", fnptr) - } - ApiStringFormat::Enum(strvec) => { - write!(f, "Enum({:?}", strvec) - } - ApiStringFormat::Pattern(regex) => { - write!(f, "Pattern({:?}", regex) - } - ApiStringFormat::Complex(schema) => { - write!(f, "Complex({:?}", schema) - } - } - } -} - -pub fn parse_boolean(value_str: &str) -> Result { - match value_str.to_lowercase().as_str() { - "1" | "on" | "yes" | "true" => Ok(true), - "0" | "off" | "no" | "false" => Ok(false), - _ => bail!("Unable to parse boolean option."), - } -} - -fn parse_property_string(value_str: &str, schema: &Schema) -> Result { - - println!("Parse property string: {}", value_str); - - let mut param_list: Vec<(String, String)> = vec![]; - - match schema { - Schema::Object(object_schema) => { - for key_val in value_str.split(',').filter(|s| !s.is_empty()) { - let kv: Vec<&str> = key_val.splitn(2, '=').collect(); - if kv.len() == 2 { - param_list.push((kv[0].into(), kv[1].into())); - } else if let Some(key) = object_schema.default_key { - param_list.push((key.into(), kv[0].into())); - } else { - bail!("Value without key, but schema does not define a default key."); - } - } - - parse_parameter_strings(¶m_list, &object_schema, true) - .map_err(Error::from) - - } - Schema::Array(array_schema) => { - let mut array : Vec = vec![]; - for value in value_str.split(',').filter(|s| !s.is_empty()) { - match parse_simple_value(value, &array_schema.items) { - Ok(res) => array.push(res), - Err(err) => bail!("unable to parse array element: {}", err), - } - } - array_schema.check_length(array.len())?; - - Ok(array.into()) - } - _ => { - bail!("Got unexpetec schema type.") - } - } -} - -pub fn parse_simple_value(value_str: &str, schema: &Schema) -> Result { - - let value = match schema { - Schema::Null => { - bail!("internal error - found Null schema."); - } - Schema::Boolean(_boolean_schema) => { - let res = parse_boolean(value_str)?; - Value::Bool(res) - } - Schema::Integer(integer_schema) => { - let res: isize = value_str.parse()?; - integer_schema.check_constraints(res)?; - Value::Number(res.into()) - } - Schema::String(string_schema) => { - string_schema.check_constraints(value_str)?; - Value::String(value_str.into()) - } - _ => bail!("unable to parse complex (sub) objects."), - }; - Ok(value) -} - -pub fn parse_parameter_strings(data: &[(String, String)], schema: &ObjectSchema, test_required: bool) -> Result { - - let mut params = json!({}); - - let mut errors = ParameterError::new(); - - let additional_properties = schema.additional_properties; - - for (key, value) in data { - if let Some((_optional, prop_schema)) = schema.lookup(&key) { - match prop_schema { - Schema::Array(array_schema) => { - if params[key] == Value::Null { - params[key] = json!([]); - } - match params[key] { - Value::Array(ref mut array) => { - match parse_simple_value(value, &array_schema.items) { - Ok(res) => array.push(res), // fixme: check_length?? - Err(err) => errors.push(format_err!("parameter '{}': {}", key, err)), - } - } - _ => errors.push(format_err!("parameter '{}': expected array - type missmatch", key)), - } - } - _ => { - match parse_simple_value(value, prop_schema) { - Ok(res) => { - if params[key] == Value::Null { - params[key] = res; - } else { - errors.push(format_err!("parameter '{}': duplicate parameter.", key)); - } - }, - Err(err) => errors.push(format_err!("parameter '{}': {}", key, err)), - } - } - } - } else if additional_properties { - match params[key] { - Value::Null => { - params[key] = Value::String(value.to_owned()); - }, - Value::String(ref old) => { - params[key] = Value::Array( - vec![Value::String(old.to_owned()), Value::String(value.to_owned())]); - } - Value::Array(ref mut array) => { - array.push(Value::String(value.to_string())); - } - _ => errors.push(format_err!("parameter '{}': expected array - type missmatch", key)), - } - } else { - errors.push(format_err!("parameter '{}': schema does not allow additional properties.", key)); - } - } - - if test_required && errors.len() == 0 { - for (name, optional, _prop_schema) in schema.properties { - if !(*optional) && params[name] == Value::Null { - errors.push(format_err!("parameter '{}': parameter is missing and it is not optional.", name)); - } - } - } - - if !errors.is_empty() { - Err(errors) - } else { - Ok(params) - } -} - -pub fn parse_query_string(query: &str, schema: &ObjectSchema, test_required: bool) -> Result { - - let param_list: Vec<(String, String)> = - form_urlencoded::parse(query.as_bytes()).into_owned().collect(); - - parse_parameter_strings(¶m_list, schema, test_required) -} - -pub fn verify_json(data: &Value, schema: &Schema) -> Result<(), Error> { - - match schema { - Schema::Object(object_schema) => { - verify_json_object(data, &object_schema)?; - } - Schema::Array(array_schema) => { - verify_json_array(data, &array_schema)?; - } - Schema::Null => { - if !data.is_null() { - bail!("Expected Null, but value is not Null."); - } - } - Schema::Boolean(boolean_schema) => verify_json_boolean(data, &boolean_schema)?, - Schema::Integer(integer_schema) => verify_json_integer(data, &integer_schema)?, - Schema::String(string_schema) => verify_json_string(data, &string_schema)?, - } - Ok(()) -} - -pub fn verify_json_string(data: &Value, schema: &StringSchema) -> Result<(), Error> { - if let Some(value) = data.as_str() { - schema.check_constraints(value) - } else { - bail!("Expected string value."); - } -} - -pub fn verify_json_boolean(data: &Value, _schema: &BooleanSchema) -> Result<(), Error> { - if !data.is_boolean() { - bail!("Expected boolean value."); - } - Ok(()) -} - -pub fn verify_json_integer(data: &Value, schema: &IntegerSchema) -> Result<(), Error> { - if let Some(value) = data.as_i64() { - schema.check_constraints(value as isize) - } else { - bail!("Expected integer value."); - } -} - -pub fn verify_json_array(data: &Value, schema: &ArraySchema) -> Result<(), Error> { - - let list = match data { - Value::Array(ref list) => list, - Value::Object(_) => bail!("Expected array - got object."), - _ => bail!("Expected array - got scalar value."), - }; - - schema.check_length(list.len())?; - - for item in list { - verify_json(item, &schema.items)?; - } - - Ok(()) -} - -pub fn verify_json_object(data: &Value, schema: &ObjectSchema) -> Result<(), Error> { - - let map = match data { - Value::Object(ref map) => map, - Value::Array(_) => bail!("Expected object - got array."), - _ => bail!("Expected object - got scalar value."), - }; - - let additional_properties = schema.additional_properties; - - for (key, value) in map { - if let Some((_optional, prop_schema)) = schema.lookup(&key) { - match prop_schema { - Schema::Object(object_schema) => { - verify_json_object(value, object_schema)?; - } - Schema::Array(array_schema) => { - verify_json_array(value, array_schema)?; - } - _ => verify_json(value, prop_schema)?, - } - } else if !additional_properties { - bail!("property '{}': schema does not allow additional properties.", key); - } - } - - for (name, optional, _prop_schema) in schema.properties { - if !(*optional) && data[name] == Value::Null { - bail!("property '{}': property is missing and it is not optional.", name); - } - } - - Ok(()) -} - -#[test] -fn test_schema1() { - let schema = Schema::Object(ObjectSchema { - description: "TEST", - additional_properties: false, - properties: &[], - default_key: None, - }); - - println!("TEST Schema: {:?}", schema); -} - -#[test] -fn test_query_string() { - - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[("name", false, &StringSchema::new("Name.").schema())] - ); - - let res = parse_query_string("", &SCHEMA, true); - assert!(res.is_err()); - } - - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[("name", true, &StringSchema::new("Name.").schema())] - ); - - let res = parse_query_string("", &SCHEMA, true); - assert!(res.is_ok()); - } - - // TEST min_length and max_length - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("name", true, &StringSchema::new("Name.") - .min_length(5) - .max_length(10) - .schema() - ), - ]); - - let res = parse_query_string("name=abcd", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("name=abcde", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("name=abcdefghijk", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("name=abcdefghij", &SCHEMA, true); - assert!(res.is_ok()); - } - - // TEST regex pattern - const_regex! { - TEST_REGEX = "test"; - TEST2_REGEX = "^test$"; - } - - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("name", false, &StringSchema::new("Name.") - .format(&ApiStringFormat::Pattern(&TEST_REGEX)) - .schema() - ), - ]); - - let res = parse_query_string("name=abcd", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("name=ateststring", &SCHEMA, true); - assert!(res.is_ok()); - } - - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("name", false, &StringSchema::new("Name.") - .format(&ApiStringFormat::Pattern(&TEST2_REGEX)) - .schema() - ), - ]); - - let res = parse_query_string("name=ateststring", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("name=test", &SCHEMA, true); - assert!(res.is_ok()); - } - - // TEST string enums - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("name", false, &StringSchema::new("Name.") - .format(&ApiStringFormat::Enum(&["ev1", "ev2"])) - .schema() - ), - ]); - - let res = parse_query_string("name=noenum", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("name=ev1", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("name=ev2", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("name=ev3", &SCHEMA, true); - assert!(res.is_err()); - } -} - -#[test] -fn test_query_integer() { - - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("count", false, &IntegerSchema::new("Count.").schema()), - ]); - - let res = parse_query_string("", &SCHEMA, true); - assert!(res.is_err()); - } - - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("count", true, &IntegerSchema::new("Count.") - .minimum(-3) - .maximum(50) - .schema() - ), - ]); - - let res = parse_query_string("", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("count=abc", &SCHEMA, false); - assert!(res.is_err()); - - let res = parse_query_string("count=30", &SCHEMA, false); - assert!(res.is_ok()); - - let res = parse_query_string("count=-1", &SCHEMA, false); - assert!(res.is_ok()); - - let res = parse_query_string("count=300", &SCHEMA, false); - assert!(res.is_err()); - - let res = parse_query_string("count=-30", &SCHEMA, false); - assert!(res.is_err()); - - let res = parse_query_string("count=50", &SCHEMA, false); - assert!(res.is_ok()); - - let res = parse_query_string("count=-3", &SCHEMA, false); - assert!(res.is_ok()); - } -} - -#[test] -fn test_query_boolean() { - - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("force", false, &BooleanSchema::new("Force.").schema()), - ]); - - let res = parse_query_string("", &SCHEMA, true); - assert!(res.is_err()); - } - - { - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("force", true, &BooleanSchema::new("Force.").schema()), - ]); - - let res = parse_query_string("", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("a=b", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("force", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("force=yes", &SCHEMA, true); - assert!(res.is_ok()); - let res = parse_query_string("force=1", &SCHEMA, true); - assert!(res.is_ok()); - let res = parse_query_string("force=On", &SCHEMA, true); - assert!(res.is_ok()); - let res = parse_query_string("force=TRUE", &SCHEMA, true); - assert!(res.is_ok()); - let res = parse_query_string("force=TREU", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("force=NO", &SCHEMA, true); - assert!(res.is_ok()); - let res = parse_query_string("force=0", &SCHEMA, true); - assert!(res.is_ok()); - let res = parse_query_string("force=off", &SCHEMA, true); - assert!(res.is_ok()); - let res = parse_query_string("force=False", &SCHEMA, true); - assert!(res.is_ok()); - } -} - -#[test] -fn test_verify_function() { - - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("p1", false, &StringSchema::new("P1") - .format(&ApiStringFormat::VerifyFn(|value| { - if value == "test" { return Ok(()) }; - bail!("format error"); - })) - .schema() - ), - ]); - - let res = parse_query_string("p1=tes", &SCHEMA, true); - assert!(res.is_err()); - let res = parse_query_string("p1=test", &SCHEMA, true); - assert!(res.is_ok()); -} - -#[test] -fn test_verify_complex_object() { - - const NIC_MODELS: ApiStringFormat = ApiStringFormat::Enum(&["e1000", "virtio"]); - - const PARAM_SCHEMA: Schema = ObjectSchema::new( - "Properties.", - &[ - ("enable", true, &BooleanSchema::new("Enable device.").schema()), - ("model", false, &StringSchema::new("Ethernet device Model.") - .format(&NIC_MODELS) - .schema() - ), - ]) - .default_key("model") - .schema(); - - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("net0", false, &StringSchema::new("First Network device.") - .format(&ApiStringFormat::Complex(&PARAM_SCHEMA)) - .schema() - ), - ]); - - let res = parse_query_string("", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("test=abc", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("net0=model=abc", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("net0=model=virtio", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("net0=model=virtio,enable=1", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("net0=virtio,enable=no", &SCHEMA, true); - assert!(res.is_ok()); -} - -#[test] -fn test_verify_complex_array() { - - { - const PARAM_SCHEMA: Schema = ArraySchema::new( - "Integer List.", &IntegerSchema::new("Soemething").schema()) - .schema(); - - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("list", false, &StringSchema::new("A list on integers, comma separated.") - .format(&ApiStringFormat::Complex(&PARAM_SCHEMA)) - .schema() - ), - ]); - - let res = parse_query_string("", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("list=", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("list=abc", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("list=1", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("list=2,3,4,5", &SCHEMA, true); - assert!(res.is_ok()); - } - - { - - const PARAM_SCHEMA: Schema = ArraySchema::new( - "Integer List.", &IntegerSchema::new("Soemething").schema()) - .min_length(1) - .max_length(3) - .schema(); - - const SCHEMA: ObjectSchema = ObjectSchema::new( - "Parameters.", - &[ - ("list", false, &StringSchema::new("A list on integers, comma separated.") - .format(&ApiStringFormat::Complex(&PARAM_SCHEMA)) - .schema() - ), - ]); - - let res = parse_query_string("list=", &SCHEMA, true); - assert!(res.is_err()); - - let res = parse_query_string("list=1,2,3", &SCHEMA, true); - assert!(res.is_ok()); - - let res = parse_query_string("list=2,3,4,5", &SCHEMA, true); - assert!(res.is_err()); - } -} diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs index 37c7b2c3..87dbc249 100644 --- a/src/bin/proxmox-backup-client.rs +++ b/src/bin/proxmox-backup-client.rs @@ -1,6 +1,3 @@ -#[macro_use] -extern crate proxmox_backup; - use failure::*; use nix::unistd::{fork, ForkResult, pipe}; use std::os::unix::io::RawFd; @@ -13,12 +10,12 @@ use std::os::unix::fs::OpenOptionsExt; use proxmox::{sortable, identity}; use proxmox::tools::fs::{file_get_contents, file_get_json, file_set_contents, image_size}; +use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment}; +use proxmox::api::schema::*; use proxmox_backup::tools; use proxmox_backup::cli::*; use proxmox_backup::api2::types::*; -use proxmox_backup::api_schema::*; -use proxmox_backup::api_schema::router::*; use proxmox_backup::client::*; use proxmox_backup::backup::*; use proxmox_backup::pxar::{ self, catalog::* }; @@ -37,7 +34,7 @@ use xdg::BaseDirectories; use futures::*; use tokio::sync::mpsc; -proxmox_backup::const_regex! { +proxmox::api::const_regex! { BACKUPSPEC_REGEX = r"^([a-zA-Z0-9_-]+\.(?:pxar|img|conf|log)):(.+)$"; } diff --git a/src/server/h2service.rs b/src/server/h2service.rs index 87d9d02a..a9abf4f3 100644 --- a/src/server/h2service.rs +++ b/src/server/h2service.rs @@ -7,8 +7,9 @@ use std::task::{Context, Poll}; use futures::*; use hyper::{Body, Request, Response, StatusCode}; +use proxmox::api::{http_err, ApiFuture}; + use crate::tools; -use crate::api_schema::api_handler::*; use crate::api_schema::router::*; use crate::server::formatter::*; use crate::server::WorkerTask; @@ -35,7 +36,7 @@ impl H2Service { if self.debug { self.worker.log(msg); } } - fn handle_request(&self, req: Request) -> BoxFut { + fn handle_request(&self, req: Request) -> ApiFuture { let (parts, body) = req.into_parts();