]>
Commit | Line | Data |
---|---|---|
b489c2ce | 1 | use std::collections::HashMap; |
d00a0968 WB |
2 | use std::fmt; |
3 | use std::future::Future; | |
4 | use std::pin::Pin; | |
b489c2ce | 5 | |
5dd21ee8 | 6 | use anyhow::Error; |
d00a0968 WB |
7 | use http::request::Parts; |
8 | use http::{Method, Response}; | |
9 | use hyper::Body; | |
d7165108 | 10 | use percent_encoding::percent_decode_str; |
c0d2165d | 11 | use serde_json::Value; |
b489c2ce | 12 | |
41f3fdfe | 13 | use proxmox_schema::{ObjectSchema, ParameterSchema, ReturnType, Schema}; |
5d73e4b8 DM |
14 | |
15 | use super::Permission; | |
41f3fdfe | 16 | use 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 |
40 | pub 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 | 71 | pub type ApiAsyncHandlerFn = &'static (dyn for<'a> Fn(Value, &'static ApiMethod, &'a mut dyn RpcEnvironment) -> ApiFuture<'a> |
7dadea06 DM |
72 | + Send |
73 | + Sync); | |
74 | ||
5dd21ee8 | 75 | pub 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 |
109 | pub 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 | 121 | pub 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. | |
125 | pub enum ApiHandler { | |
126 | Sync(ApiHandlerFn), | |
7dadea06 | 127 | Async(ApiAsyncHandlerFn), |
d00a0968 WB |
128 | AsyncHttp(ApiAsyncHttpHandlerFn), |
129 | } | |
b489c2ce | 130 | |
b0ef4051 WB |
131 | #[cfg(feature = "test-harness")] |
132 | impl Eq for ApiHandler {} | |
133 | ||
134 | #[cfg(feature = "test-harness")] | |
135 | impl 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 |
165 | pub type SubdirMap = &'static [(&'static str, &'static Router)]; |
166 | ||
98708e34 | 167 | /// Classify different types of routers |
b489c2ce WB |
168 | pub 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] | |
188 | macro_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 | 231 | pub 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 | ||
244 | impl 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 | ||
375 | impl Default for Router { | |
d00a0968 | 376 | #[inline] |
b489c2ce WB |
377 | fn default() -> Self { |
378 | Self::new() | |
379 | } | |
380 | } | |
d00a0968 WB |
381 | |
382 | const NULL_SCHEMA: Schema = Schema::Null; | |
383 | ||
384 | fn 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 | ||
393 | const 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 | 397 | pub 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 |
404 | pub 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 | ||
421 | impl 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 | ||
432 | impl 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 | } |