]> git.proxmox.com Git - proxmox.git/blame - proxmox-router/src/router.rs
router: rustfmt
[proxmox.git] / proxmox-router / src / router.rs
CommitLineData
b489c2ce 1use std::collections::HashMap;
d00a0968
WB
2use std::fmt;
3use std::future::Future;
4use std::pin::Pin;
b489c2ce 5
5dd21ee8 6use anyhow::Error;
d00a0968
WB
7use http::request::Parts;
8use http::{Method, Response};
9use hyper::Body;
d7165108 10use percent_encoding::percent_decode_str;
c0d2165d 11use serde_json::Value;
b489c2ce 12
41f3fdfe 13use proxmox_schema::{ObjectSchema, ParameterSchema, ReturnType, Schema};
5d73e4b8
DM
14
15use super::Permission;
41f3fdfe 16use crate::RpcEnvironment;
bf84e756 17
d00a0968
WB
18/// A synchronous API handler gets a json Value as input and returns a json Value as output.
19///
20/// Most API handler are synchronous. Use this to define such handler:
21/// ```
fbd82c81 22/// # use anyhow::Error;
d00a0968 23/// # use serde_json::{json, Value};
41f3fdfe
WB
24/// use proxmox_router::{ApiHandler, ApiMethod, RpcEnvironment};
25/// use proxmox_schema::ObjectSchema;
26///
d00a0968
WB
27/// fn hello(
28/// param: Value,
29/// info: &ApiMethod,
30/// rpcenv: &mut dyn RpcEnvironment,
31/// ) -> Result<Value, Error> {
32/// Ok(json!("Hello world!"))
33/// }
34///
35/// const API_METHOD_HELLO: ApiMethod = ApiMethod::new(
36/// &ApiHandler::Sync(&hello),
37/// &ObjectSchema::new("Hello World Example", &[])
38/// );
39/// ```
e2d9f676
WB
40pub type ApiHandlerFn = &'static (dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result<Value, Error>
41 + Send
42 + Sync
43 + 'static);
d00a0968 44
7dadea06
DM
45/// Asynchronous API handlers
46///
47/// Returns a future Value.
48/// ```
7dadea06 49/// # use serde_json::{json, Value};
7dadea06 50/// #
41f3fdfe
WB
51/// use proxmox_router::{ApiFuture, ApiHandler, ApiMethod, RpcEnvironment};
52/// use proxmox_schema::ObjectSchema;
53///
7dadea06
DM
54///
55/// fn hello_future<'a>(
56/// param: Value,
57/// info: &ApiMethod,
58/// rpcenv: &'a mut dyn RpcEnvironment,
59/// ) -> ApiFuture<'a> {
41f3fdfe 60/// Box::pin(async move {
7dadea06
DM
61/// let data = json!("hello world!");
62/// Ok(data)
41f3fdfe 63/// })
7dadea06
DM
64/// }
65///
66/// const API_METHOD_HELLO_FUTURE: ApiMethod = ApiMethod::new(
67/// &ApiHandler::Async(&hello_future),
68/// &ObjectSchema::new("Hello World Example (async)", &[])
69/// );
70/// ```
e2d9f676 71pub type ApiAsyncHandlerFn = &'static (dyn for<'a> Fn(Value, &'static ApiMethod, &'a mut dyn RpcEnvironment) -> ApiFuture<'a>
7dadea06
DM
72 + Send
73 + Sync);
74
5dd21ee8 75pub type ApiFuture<'a> = Pin<Box<dyn Future<Output = Result<Value, anyhow::Error>> + Send + 'a>>;
7dadea06 76
d00a0968
WB
77/// Asynchronous HTTP API handlers
78///
79/// They get low level access to request and response data. Use this
80/// to implement custom upload/download functions.
81/// ```
d00a0968 82/// # use serde_json::{json, Value};
d00a0968 83/// #
d00a0968
WB
84/// use hyper::{Body, Response, http::request::Parts};
85///
41f3fdfe
WB
86/// use proxmox_router::{ApiHandler, ApiMethod, ApiResponseFuture, RpcEnvironment};
87/// use proxmox_schema::ObjectSchema;
88///
d00a0968
WB
89/// fn low_level_hello(
90/// parts: Parts,
91/// req_body: Body,
92/// param: Value,
93/// info: &ApiMethod,
94/// rpcenv: Box<dyn RpcEnvironment>,
7dadea06 95/// ) -> ApiResponseFuture {
41f3fdfe 96/// Box::pin(async move {
d00a0968
WB
97/// let response = http::Response::builder()
98/// .status(200)
99/// .body(Body::from("Hello world!"))?;
100/// Ok(response)
41f3fdfe 101/// })
d00a0968
WB
102/// }
103///
104/// const API_METHOD_LOW_LEVEL_HELLO: ApiMethod = ApiMethod::new(
105/// &ApiHandler::AsyncHttp(&low_level_hello),
106/// &ObjectSchema::new("Hello World Example (low level)", &[])
107/// );
108/// ```
7dadea06
DM
109pub type ApiAsyncHttpHandlerFn = &'static (dyn Fn(
110 Parts,
111 Body,
112 Value,
113 &'static ApiMethod,
114 Box<dyn RpcEnvironment>,
115) -> ApiResponseFuture
d00a0968
WB
116 + Send
117 + Sync
118 + 'static);
119
98708e34 120/// The output of an asynchronous API handler is a future yielding a `Response`.
7dadea06 121pub type ApiResponseFuture =
5dd21ee8 122 Pin<Box<dyn Future<Output = Result<Response<Body>, anyhow::Error>> + Send>>;
d00a0968
WB
123
124/// Enum for different types of API handler functions.
125pub enum ApiHandler {
126 Sync(ApiHandlerFn),
7dadea06 127 Async(ApiAsyncHandlerFn),
d00a0968
WB
128 AsyncHttp(ApiAsyncHttpHandlerFn),
129}
b489c2ce 130
b0ef4051
WB
131#[cfg(feature = "test-harness")]
132impl Eq for ApiHandler {}
133
134#[cfg(feature = "test-harness")]
135impl PartialEq for ApiHandler {
136 fn eq(&self, rhs: &Self) -> bool {
137 unsafe {
138 match (self, rhs) {
139 (ApiHandler::Sync(l), ApiHandler::Sync(r)) => {
140 core::mem::transmute::<_, usize>(l) == core::mem::transmute::<_, usize>(r)
141 }
142 (ApiHandler::Async(l), ApiHandler::Async(r)) => {
143 core::mem::transmute::<_, usize>(l) == core::mem::transmute::<_, usize>(r)
144 }
145 (ApiHandler::AsyncHttp(l), ApiHandler::AsyncHttp(r)) => {
146 core::mem::transmute::<_, usize>(l) == core::mem::transmute::<_, usize>(r)
147 }
148 _ => false,
149 }
150 }
151 }
152}
153
f0246203
DM
154/// Lookup table to child `Router`s
155///
156/// Stores a sorted list of `(name, router)` tuples:
157///
158/// - `name`: The name of the subdir
159/// - `router`: The router for this subdir
160///
98708e34 161/// **Note:** The list has to be sorted by name, because we use a binary
f0246203
DM
162/// search to find items.
163///
164/// This is a workaround unless RUST can const_fn `Hash::new()`
b489c2ce
WB
165pub type SubdirMap = &'static [(&'static str, &'static Router)];
166
98708e34 167/// Classify different types of routers
b489c2ce
WB
168pub enum SubRoute {
169 //Hash(HashMap<String, Router>),
f0246203
DM
170 /// Router with static lookup map.
171 ///
172 /// The first path element is used to lookup a new
a14c7b17 173 /// router with `SubdirMap`. If found, the remaining path is
f0246203 174 /// passed to that router.
b489c2ce 175 Map(SubdirMap),
f0246203
DM
176 /// Router that always match the first path element
177 ///
178 /// The matched path element is stored as parameter
a14c7b17 179 /// `param_name`. The remaining path is matched using the `router`.
b489c2ce
WB
180 MatchAll {
181 router: &'static Router,
182 param_name: &'static str,
183 },
184}
185
186/// Macro to create an ApiMethod to list entries from SubdirMap
187#[macro_export]
188macro_rules! list_subdirs_api_method {
189 ($map:expr) => {
41f3fdfe
WB
190 $crate::ApiMethod::new(
191 &$crate::ApiHandler::Sync( & |_, _, _| {
a6ce1e43
WB
192 let index = ::serde_json::json!(
193 $map.iter().map(|s| ::serde_json::json!({ "subdir": s.0}))
194 .collect::<Vec<::serde_json::Value>>()
b489c2ce
WB
195 );
196 Ok(index)
197 }),
41f3fdfe 198 &$crate::ListSubdirsObjectSchema::new("Directory index.", &[])
436bf05e 199 .additional_properties(true)
41f3fdfe 200 ).access(None, &$crate::Permission::Anybody)
b489c2ce
WB
201 }
202}
203
f0246203
DM
204/// Define APIs with routing information
205///
206/// REST APIs use hierarchical paths to identify resources. A path
207/// consists of zero or more components, separated by `/`. A `Router`
208/// is a simple data structure to define such APIs. Each `Router` is
209/// responsible for a specific path, and may define `ApiMethod`s for
210/// different HTTP requests (GET, PUT, POST, DELETE). If the path
211/// contains more elements, `subroute` is used to find the correct
212/// endpoint.
6d31db9a
DM
213///
214/// Routers are meant to be build a compile time, and you can use
215/// all `const fn(mut self, ..)` methods to configure them.
216///
217///```
6d31db9a 218/// # use serde_json::{json, Value};
41f3fdfe
WB
219/// use proxmox_router::{ApiHandler, ApiMethod, Router};
220/// use proxmox_schema::ObjectSchema;
05cad892 221///
6d31db9a
DM
222/// const API_METHOD_HELLO: ApiMethod = ApiMethod::new(
223/// &ApiHandler::Sync(&|_, _, _| {
224/// Ok(json!("Hello world!"))
225/// }),
226/// &ObjectSchema::new("Hello World Example", &[])
227/// );
228/// const ROUTER: Router = Router::new()
229/// .get(&API_METHOD_HELLO);
230///```
b489c2ce 231pub struct Router {
f0246203 232 /// GET requests
b489c2ce 233 pub get: Option<&'static ApiMethod>,
f0246203 234 /// PUT requests
b489c2ce 235 pub put: Option<&'static ApiMethod>,
f0246203 236 /// POST requests
b489c2ce 237 pub post: Option<&'static ApiMethod>,
f0246203 238 /// DELETE requests
b489c2ce 239 pub delete: Option<&'static ApiMethod>,
f0246203 240 /// Used to find the correct API endpoint.
b489c2ce
WB
241 pub subroute: Option<SubRoute>,
242}
243
244impl Router {
ed426cd9 245 /// Create a new Router.
b489c2ce
WB
246 pub const fn new() -> Self {
247 Self {
248 get: None,
249 put: None,
250 post: None,
251 delete: None,
252 subroute: None,
253 }
254 }
255
ed426cd9 256 /// Configure a static map as `subroute`.
b489c2ce
WB
257 pub const fn subdirs(mut self, map: SubdirMap) -> Self {
258 self.subroute = Some(SubRoute::Map(map));
259 self
260 }
261
ed426cd9 262 /// Configure a `SubRoute::MatchAll` as `subroute`.
b489c2ce
WB
263 pub const fn match_all(mut self, param_name: &'static str, router: &'static Router) -> Self {
264 self.subroute = Some(SubRoute::MatchAll { router, param_name });
265 self
266 }
267
ed426cd9 268 /// Configure the GET method.
b489c2ce
WB
269 pub const fn get(mut self, m: &'static ApiMethod) -> Self {
270 self.get = Some(m);
271 self
272 }
273
ed426cd9 274 /// Configure the PUT method.
b489c2ce
WB
275 pub const fn put(mut self, m: &'static ApiMethod) -> Self {
276 self.put = Some(m);
277 self
278 }
279
ed426cd9 280 /// Configure the POST method.
b489c2ce
WB
281 pub const fn post(mut self, m: &'static ApiMethod) -> Self {
282 self.post = Some(m);
283 self
284 }
285
ed426cd9 286 /// Same as `post`, but expects an `AsyncHttp` handler.
b489c2ce 287 pub const fn upload(mut self, m: &'static ApiMethod) -> Self {
ed426cd9 288 // fixme: expect AsyncHttp
b489c2ce
WB
289 self.post = Some(m);
290 self
291 }
292
ed426cd9 293 /// Same as `get`, but expects an `AsyncHttp` handler.
b489c2ce 294 pub const fn download(mut self, m: &'static ApiMethod) -> Self {
ed426cd9 295 // fixme: expect AsyncHttp
b489c2ce
WB
296 self.get = Some(m);
297 self
298 }
299
ed426cd9 300 /// Same as `get`, but expects an `AsyncHttp` handler.
b489c2ce 301 pub const fn upgrade(mut self, m: &'static ApiMethod) -> Self {
ed426cd9 302 // fixme: expect AsyncHttp
b489c2ce
WB
303 self.get = Some(m);
304 self
305 }
306
ed426cd9 307 /// Configure the DELETE method
b489c2ce
WB
308 pub const fn delete(mut self, m: &'static ApiMethod) -> Self {
309 self.delete = Some(m);
310 self
311 }
312
98708e34 313 /// Find the router for a specific path.
ed426cd9
DM
314 ///
315 /// - `components`: Path, split into individual components.
98708e34 316 /// - `uri_param`: Mutable hash map to store parameter from `MatchAll` router.
b489c2ce
WB
317 pub fn find_route(
318 &self,
319 components: &[&str],
320 uri_param: &mut HashMap<String, String>,
321 ) -> Option<&Router> {
322 if components.is_empty() {
323 return Some(self);
324 };
325
a14c7b17 326 let (dir, remaining) = (components[0], &components[1..]);
b489c2ce 327
d7165108
DC
328 let dir = match percent_decode_str(dir).decode_utf8() {
329 Ok(dir) => dir.to_string(),
330 Err(_) => return None,
331 };
332
b489c2ce
WB
333 match self.subroute {
334 None => {}
335 Some(SubRoute::Map(dirmap)) => {
d7165108 336 if let Ok(ind) = dirmap.binary_search_by_key(&dir.as_str(), |(name, _)| name) {
b489c2ce
WB
337 let (_name, router) = dirmap[ind];
338 //println!("FOUND SUBDIR {}", dir);
a14c7b17 339 return router.find_route(remaining, uri_param);
b489c2ce
WB
340 }
341 }
342 Some(SubRoute::MatchAll { router, param_name }) => {
343 //println!("URI PARAM {} = {}", param_name, dir); // fixme: store somewhere
d7165108 344 uri_param.insert(param_name.to_owned(), dir);
a14c7b17 345 return router.find_route(remaining, uri_param);
b489c2ce
WB
346 }
347 }
348
349 None
350 }
351
ed426cd9
DM
352 /// Lookup the API method for a specific path.
353 /// - `components`: Path, split into individual components.
354 /// - `method`: The HTTP method.
98708e34 355 /// - `uri_param`: Mutable hash map to store parameter from `MatchAll` router.
b489c2ce
WB
356 pub fn find_method(
357 &self,
358 components: &[&str],
359 method: Method,
360 uri_param: &mut HashMap<String, String>,
361 ) -> Option<&ApiMethod> {
362 if let Some(info) = self.find_route(components, uri_param) {
363 return match method {
364 Method::GET => info.get,
365 Method::PUT => info.put,
366 Method::POST => info.post,
367 Method::DELETE => info.delete,
368 _ => None,
369 };
370 }
371 None
372 }
373}
374
375impl Default for Router {
d00a0968 376 #[inline]
b489c2ce
WB
377 fn default() -> Self {
378 Self::new()
379 }
380}
d00a0968
WB
381
382const NULL_SCHEMA: Schema = Schema::Null;
383
384fn dummy_handler_fn(
385 _arg: Value,
386 _method: &ApiMethod,
387 _env: &mut dyn RpcEnvironment,
388) -> Result<Value, Error> {
389 // do nothing
390 Ok(Value::Null)
391}
392
393const DUMMY_HANDLER: ApiHandler = ApiHandler::Sync(&dummy_handler_fn);
394
7ec6448d 395/// Access permission with description
b67e2f72 396#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
973e7cce 397pub struct ApiAccess {
e78e31ab 398 pub description: Option<&'static str>,
7ec6448d
DM
399 pub permission: &'static Permission,
400}
401
98708e34 402/// This struct defines a synchronous API call which returns the result as json `Value`
b0ef4051 403#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
d00a0968
WB
404pub struct ApiMethod {
405 /// The protected flag indicates that the provides function should be forwarded
98708e34 406 /// to the daemon running in privileged mode.
d00a0968
WB
407 pub protected: bool,
408 /// This flag indicates that the provided method may change the local timezone, so the server
409 /// should do a tzset afterwards
410 pub reload_timezone: bool,
411 /// Parameter type Schema
0cdd47c8 412 pub parameters: ParameterSchema,
d00a0968 413 /// Return type Schema
e8998851 414 pub returns: ReturnType,
d00a0968
WB
415 /// Handler function
416 pub handler: &'static ApiHandler,
7ec6448d 417 /// Access Permissions
973e7cce 418 pub access: ApiAccess,
d00a0968
WB
419}
420
421impl std::fmt::Debug for ApiMethod {
422 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
423 write!(f, "ApiMethod {{ ")?;
424 write!(f, " parameters: {:?}", self.parameters)?;
425 write!(f, " returns: {:?}", self.returns)?;
426 write!(f, " handler: {:p}", &self.handler)?;
7ec6448d 427 write!(f, " permissions: {:?}", &self.access.permission)?;
d00a0968
WB
428 write!(f, "}}")
429 }
430}
431
432impl ApiMethod {
0cdd47c8 433 pub const fn new_full(handler: &'static ApiHandler, parameters: ParameterSchema) -> Self {
d00a0968
WB
434 Self {
435 parameters,
436 handler,
e8998851 437 returns: ReturnType::new(false, &NULL_SCHEMA),
d00a0968
WB
438 protected: false,
439 reload_timezone: false,
973e7cce 440 access: ApiAccess {
e78e31ab 441 description: None,
973e7cce 442 permission: &Permission::Superuser,
7ec6448d 443 },
d00a0968
WB
444 }
445 }
446
0cdd47c8
WB
447 pub const fn new(handler: &'static ApiHandler, parameters: &'static ObjectSchema) -> Self {
448 Self::new_full(handler, ParameterSchema::Object(parameters))
449 }
450
d00a0968
WB
451 pub const fn new_dummy(parameters: &'static ObjectSchema) -> Self {
452 Self {
0cdd47c8 453 parameters: ParameterSchema::Object(parameters),
d00a0968 454 handler: &DUMMY_HANDLER,
e8998851 455 returns: ReturnType::new(false, &NULL_SCHEMA),
d00a0968
WB
456 protected: false,
457 reload_timezone: false,
973e7cce 458 access: ApiAccess {
e78e31ab 459 description: None,
973e7cce 460 permission: &Permission::Superuser,
7ec6448d 461 },
d00a0968
WB
462 }
463 }
464
e8998851
WB
465 pub const fn returns(mut self, returns: ReturnType) -> Self {
466 self.returns = returns;
d00a0968
WB
467
468 self
469 }
470
471 pub const fn protected(mut self, protected: bool) -> Self {
472 self.protected = protected;
473
474 self
475 }
476
477 pub const fn reload_timezone(mut self, reload_timezone: bool) -> Self {
478 self.reload_timezone = reload_timezone;
479
480 self
481 }
7ec6448d 482
973e7cce
WB
483 pub const fn access(
484 mut self,
e78e31ab 485 description: Option<&'static str>,
973e7cce
WB
486 permission: &'static Permission,
487 ) -> Self {
488 self.access = ApiAccess {
489 description,
490 permission,
491 };
7ec6448d
DM
492
493 self
494 }
d00a0968 495}