ic_pluto/http.rs
1use crate::{
2 cors::Cors,
3 method::Method,
4 router::{HandlerContainer, Router},
5};
6use candid::{CandidType, Deserialize};
7use matchit::{Match, Params as MatchitParams};
8use serde::Serialize;
9use serde_json::{json, Value};
10use std::{collections::HashMap, str::FromStr};
11
12/// HeaderField is the type of the header of the request.
13#[derive(CandidType, Deserialize, Clone)]
14pub struct HeaderField(String, String);
15
16/// RawHttpRequest is the request type that is sent by the client.
17/// It is a raw version of HttpRequest. It is compatible with the Candid type.
18/// It is used in the 'http_request' and 'http_request_update' function of the canister and it is provided by the IC.
19/// It is converted to HttpRequest before it is used in the handler.
20#[derive(CandidType, Deserialize, Clone)]
21pub struct RawHttpRequest {
22 pub(crate) method: String,
23 pub(crate) url: String,
24 pub(crate) headers: Vec<HeaderField>,
25 #[serde(with = "serde_bytes")]
26 pub(crate) body: Vec<u8>,
27}
28
29impl From<RawHttpRequest> for HttpRequest {
30 fn from(req: RawHttpRequest) -> Self {
31 HttpRequest {
32 method: req.method,
33 url: req.url,
34 headers: req.headers,
35 body: req.body.clone(),
36 params: HashMap::new(),
37 path: String::new(),
38 }
39 }
40}
41
42#[derive(CandidType, Deserialize, Clone)]
43/// HttpRequest is the request type that is available in handler.
44/// It is a more user-friendly version of RawHttpRequest
45/// It is used in handler to allow user to process the request.
46pub struct HttpRequest {
47 pub method: String,
48 pub url: String,
49 pub headers: Vec<HeaderField>,
50 #[serde(with = "serde_bytes")]
51 pub body: Vec<u8>,
52 pub params: HashMap<String, String>,
53 pub path: String,
54}
55
56impl HttpRequest {
57 pub fn body_into_struct<T: for<'a> Deserialize<'a>>(&self) -> Result<T, HttpResponse> {
58 serde_json::from_slice(&self.body).map_err(|msg| HttpResponse {
59 status_code: 400,
60 headers: HashMap::new(),
61 body: json!({
62 "statusCode": 400,
63 "message": msg.to_string(),
64 })
65 .into(),
66 })
67 }
68
69 pub fn params_into_struct<T: for<'a> Deserialize<'a>>(&self) -> Result<T, HttpResponse> {
70 let json = serde_json::json!(&self.params);
71 serde_json::from_value(json).map_err(|msg| HttpResponse {
72 status_code: 400,
73 headers: HashMap::new(),
74 body: json!({
75 "statusCode": 400,
76 "message": msg.to_string(),
77 })
78 .into(),
79 })
80 }
81}
82
83/// RawHttpResponse is the response type that is sent back to the client.
84/// It is a raw version of HttpResponse. It is compatible with the Candid type.
85#[derive(CandidType, Deserialize)]
86pub struct RawHttpResponse {
87 pub(crate) status_code: u16,
88 pub(crate) headers: HashMap<String, String>,
89 #[serde(with = "serde_bytes")]
90 pub(crate) body: Vec<u8>,
91 pub(crate) upgrade: Option<bool>,
92}
93
94impl RawHttpResponse {
95 /// Set the upgrade flag of the response.
96 fn set_upgrade(&mut self, upgrade: bool) {
97 self.upgrade = Some(upgrade);
98 }
99
100 /// Enrich the header of the response depending on the content the body.
101 fn enrich_header(&mut self) {
102 if let None = self.headers.get("Content-Type") {
103 self.headers.insert(
104 String::from("Content-Type"),
105 String::from("application/json"),
106 );
107 }
108 self.headers
109 .insert(String::from("X-Powered-By"), String::from("Pluto"));
110 }
111}
112
113#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
114pub enum HttpBody {
115 Value(Value),
116 String(String),
117 Raw(Vec<u8>),
118}
119
120impl From<HttpBody> for Vec<u8> {
121 fn from(b: HttpBody) -> Self {
122 return match b {
123 HttpBody::Value(json) => json.to_string().into_bytes().into(),
124 HttpBody::String(string) => string.into_bytes().into(),
125 HttpBody::Raw(vec) => vec,
126 };
127 }
128}
129
130impl From<String> for HttpBody {
131 fn from(s: String) -> Self {
132 HttpBody::String(s)
133 }
134}
135
136impl From<Value> for HttpBody {
137 fn from(j: Value) -> Self {
138 HttpBody::Value(j)
139 }
140}
141
142impl From<Vec<u8>> for HttpBody {
143 fn from(value: Vec<u8>) -> Self {
144 Self::Raw(value)
145 }
146}
147
148/// HttpResponse is the response type that is available in handler.
149/// It is a more user-friendly version of RawHttpResponse
150/// After the handler is executed, it is converted to RawHttpResponse.
151#[derive(Debug, PartialEq, Clone)]
152pub struct HttpResponse {
153 pub status_code: u16,
154 pub headers: HashMap<String, String>,
155 pub body: HttpBody,
156}
157
158impl HttpResponse {
159 /// Add a header to the response.
160 /// If the header already exists, it will be overwritten.
161 pub fn add_raw_header(&mut self, key: &str, value: String) {
162 self.headers.insert(key.to_string(), value);
163 }
164
165 /// Remove a header from the response.
166 /// If the header does not exist, nothing will happen.
167 pub fn remove_header(&mut self, key: &str) {
168 self.headers.remove(key);
169 }
170}
171
172impl From<HttpResponse> for RawHttpResponse {
173 fn from(res: HttpResponse) -> Self {
174 let mut res = RawHttpResponse {
175 status_code: res.status_code,
176 headers: res.headers,
177 body: res.body.into(),
178 upgrade: Some(false),
179 };
180 res.enrich_header();
181 res
182 }
183}
184
185/// This macro is used to create a new instance of HttpServe with given router.
186/// It is used in the 'http_request' and 'http_request_update' function of the canister.
187/// This macro handles routing from not upgradable request to upgradable request.
188///
189/// # Example
190///
191/// ```rust
192/// use ic_cdk::{query, update};
193///
194/// use pluto::router::Router;
195/// use pluto::http_serve_router;
196/// use pluto::http::{RawHttpRequest, RawHttpResponse};
197/// use pluto::http::HttpServe;
198///
199/// #[query]
200/// async fn http_request(req: RawHttpRequest) -> RawHttpResponse {
201/// let router = setup_router();
202/// http_serve_router!(router).serve(req).await
203/// }
204///
205/// #[update]
206/// async fn http_request_update(req: RawHttpRequest) -> RawHttpResponse {
207/// let router = setup_router();
208/// http_serve_router!(router).serve(req).await
209/// }
210///
211/// fn setup_router() -> Router {
212/// Router::new()
213/// }
214/// ```
215#[macro_export]
216macro_rules! http_serve_router {
217 ($arg:expr) => {{
218 fn f() {}
219 fn type_name_of<T>(_: T) -> &'static str {
220 std::any::type_name::<T>()
221 }
222 let name = type_name_of(f);
223 let name = &name[..name.len() - 3];
224 let http_request_type;
225 if name.contains("http_request::{{closure}}") {
226 http_request_type = "http_request"
227 } else if name.contains("http_request_update::{{closure}}") {
228 http_request_type = "http_request_update"
229 } else {
230 panic!("Function \"http_request\" not found")
231 }
232 HttpServe::new_with_router($arg, http_request_type)
233 }};
234}
235
236/// This macro is used to create a new instance of HttpServe.
237/// It is used in the 'http_request' and 'http_request_update' function of the canister.
238/// This macro handles routing from not upgradable request to upgradable request.
239///
240/// # Example
241///
242/// ```rust
243/// use ic_cdk::{query, update};
244///
245/// use pluto::router::Router;
246/// use pluto::http_serve;
247/// use pluto::http::{RawHttpRequest, RawHttpResponse};
248/// use pluto::http::HttpServe;
249///
250/// #[query]
251/// async fn http_request(req: RawHttpRequest) -> RawHttpResponse {
252/// bootstrap(http_serve!(), req).await
253/// }
254///
255/// #[update]
256/// async fn http_request_update(req: RawHttpRequest) -> RawHttpResponse {
257/// bootstrap(http_serve!(), req).await
258/// }
259///
260/// async fn bootstrap(mut app: HttpServe, req: RawHttpRequest) -> RawHttpResponse {
261/// let router = Router::new();
262/// app.set_router(router);
263/// app.serve(req).await
264/// }
265/// ```
266#[macro_export]
267macro_rules! http_serve {
268 () => {{
269 fn f() {}
270 fn type_name_of<T>(_: T) -> &'static str {
271 std::any::type_name::<T>()
272 }
273 let name = type_name_of(f);
274 let name = &name[..name.len() - 3];
275 let http_request_type;
276 if name.contains("http_request::{{closure}}") {
277 http_request_type = "http_request"
278 } else if name.contains("http_request_update::{{closure}}") {
279 http_request_type = "http_request_update"
280 } else {
281 panic!("Function \"http_request\" not found")
282 }
283 HttpServe::new(http_request_type)
284 }};
285}
286
287/// HttpServe is the main struct of the Pluto library.
288/// It is used to create a new instance of HttpServe.
289/// It is used in the 'http_request' and 'http_request_update' function of the canister.
290/// This struct handles routing from not upgradable request to upgradable request.
291/// It also handles CORS.
292pub struct HttpServe {
293 router: Router,
294 cors_policy: Option<Cors>,
295 is_query: bool,
296}
297
298impl HttpServe {
299 /// Create a new instance of HttpServe depending on the function name.
300 pub fn new(init_name: &str) -> Self {
301 let created_in_query = match init_name {
302 "http_request_update" => false,
303 &_ => true,
304 };
305 Self {
306 router: Router::new(),
307 cors_policy: None,
308 is_query: created_in_query,
309 }
310 }
311
312 /// Create a new instance of HttpServe with given router.
313 pub fn new_with_router(r: Router, init_name: &str) -> Self {
314 let created_in_query = match init_name {
315 "http_request_update" => false,
316 &_ => true,
317 };
318 Self {
319 router: r,
320 cors_policy: None,
321 is_query: created_in_query,
322 }
323 }
324
325 /// Set the router of the HttpServe.
326 pub fn set_router(&mut self, r: Router) {
327 self.router = r;
328 }
329
330 /// Add a handler to the router.
331 /// The handler will be executed if the request do matches any method and path.
332 pub fn bad_request_error(error: serde_json::Value) -> Result<(), HttpResponse> {
333 return Err(HttpResponse {
334 status_code: 400,
335 headers: HashMap::new(),
336 body: json!({
337 "statusCode": 400,
338 "message": "Bad Request",
339 "error": error
340 })
341 .into(),
342 });
343 }
344
345 /// Predefined server error response.
346 pub fn internal_server_error() -> Result<(), HttpResponse> {
347 return Err(HttpResponse {
348 status_code: 500,
349 headers: HashMap::new(),
350 body: json!({
351 "statusCode": 500,
352 "message": "Internal server error",
353 })
354 .into(),
355 });
356 }
357
358 /// Predefined not found error response.
359 pub fn not_found_error(message: String) -> Result<(), HttpResponse> {
360 return Err(HttpResponse {
361 status_code: 404,
362 headers: HashMap::new(),
363 body: json!({
364 "statusCode": 404,
365 "message": message,
366 "error": "Not Found"
367 })
368 .into(),
369 });
370 }
371
372 fn get_path(url: &str) -> &str {
373 let mut path = url.split('?').next().unwrap_or("");
374 if path.ends_with("/") {
375 let mut chars = path.chars();
376 chars.next_back();
377 path = chars.as_str();
378 }
379 path
380 }
381
382 fn params_to_string(params: MatchitParams) -> HashMap<String, String> {
383 let mut param: HashMap<String, String> = HashMap::new();
384 for val in params.iter() {
385 param.insert(String::from(val.0), String::from(val.1));
386 }
387 param
388 }
389
390 async fn build_and_execute_request(
391 self,
392 req: RawHttpRequest,
393 path: &str,
394 lookup: Match<'_, '_, &HandlerContainer>,
395 upgrade: bool,
396 ) -> RawHttpResponse {
397 let mut req: HttpRequest = req.into();
398 req.path = String::from(path);
399 req.params = Self::params_to_string(lookup.params);
400 let handle_res = lookup.value.handler.handle(req).await;
401 let mut res = Self::unwrap_response(handle_res);
402 self.use_res_plugins(&mut res);
403 let mut raw_res: RawHttpResponse = res.into();
404 raw_res.set_upgrade(upgrade);
405 raw_res
406 }
407
408 fn unwrap_response(res: Result<HttpResponse, HttpResponse>) -> HttpResponse {
409 match res {
410 Ok(res) => res,
411 Err(err_res) => err_res,
412 }
413 }
414
415 fn use_res_plugins(self, res: &mut HttpResponse) {
416 self.add_cors_to_res(res);
417 }
418
419 fn add_cors_to_res(self, res: &mut HttpResponse) {
420 if let Some(cors) = self.cors_policy {
421 cors.merge(res)
422 }
423 }
424
425 /// Set the CORS policy of the HttpServe.
426 /// ```rust
427 /// use ic_cdk::{query, update};
428 ///
429 /// use pluto::router::Router;
430 /// use pluto::http_serve;
431 /// use pluto::http::{RawHttpRequest, RawHttpResponse};
432 /// use pluto::http::HttpServe;
433 /// use pluto::method::Method;
434 /// use pluto::cors::Cors;
435 ///
436 /// #[query]
437 /// async fn http_request(req: RawHttpRequest) -> RawHttpResponse {
438 /// bootstrap(http_serve!(), req).await
439 /// }
440 ///
441 /// #[update]
442 /// async fn http_request_update(req: RawHttpRequest) -> RawHttpResponse {
443 /// bootstrap(http_serve!(), req).await
444 /// }
445 ///
446 /// async fn bootstrap(mut app: HttpServe, req: RawHttpRequest) -> RawHttpResponse {
447 /// let router = Router::new();
448 /// let cors = Cors::new()
449 /// .allow_origin("*")
450 /// .allow_methods(vec![Method::POST, Method::PUT])
451 /// .allow_headers(vec!["Content-Type", "Authorization"])
452 /// .max_age(Some(3600));
453 ///
454 /// app.set_router(router);
455 /// app.use_cors(cors);
456 /// app.serve(req).await
457 /// }
458 /// ```
459 pub fn use_cors(&mut self, cors_policy: Cors) {
460 self.cors_policy = Some(cors_policy);
461 }
462
463 /// Serve the request.
464 /// It will return a RawHttpResponse.
465 /// It will return an internal server error if the request is not valid.
466 /// It will return a not found error if the request does not match any method and path.
467 /// ```rust
468 /// use ic_cdk::{query, update};
469 ///
470 /// use pluto::router::Router;
471 /// use pluto::http_serve;
472 /// use pluto::http::{RawHttpRequest, RawHttpResponse};
473 /// use pluto::http::HttpServe;
474 ///
475 /// #[query]
476 /// async fn http_request(req: RawHttpRequest) -> RawHttpResponse {
477 /// bootstrap(http_serve!(), req).await
478 /// }
479 ///
480 /// #[update]
481 /// async fn http_request_update(req: RawHttpRequest) -> RawHttpResponse {
482 /// bootstrap(http_serve!(), req).await
483 /// }
484 ///
485 /// async fn bootstrap(mut app: HttpServe, req: RawHttpRequest) -> RawHttpResponse {
486 /// let router = Router::new();
487 /// app.set_router(router);
488 /// app.serve(req).await
489 /// }
490 /// ```
491 pub async fn serve(self, req: RawHttpRequest) -> RawHttpResponse {
492 match Method::from_str(req.method.as_ref()) {
493 Err(_) => Self::internal_server_error().unwrap_err().into(),
494 Ok(method) => {
495 let path = Self::get_path(req.url.as_ref());
496 match self.router.clone().lookup(method, path) {
497 Err(message) => {
498 // Handle OPTIONS request
499 if req.method == Method::OPTIONS.to_string() && self.router.handle_options {
500 let router_clone = self.router.clone();
501 let allow = router_clone.allowed(path);
502
503 if !allow.is_empty() {
504 return match self.router.global_options {
505 Some(ref handler) => {
506 let handle_res = handler.handler.handle(req.into()).await;
507 let mut raw_res: RawHttpResponse =
508 Self::unwrap_response(handle_res).into();
509 raw_res.set_upgrade(handler.upgrade);
510 raw_res
511 }
512 None => {
513 let mut res = HttpResponse {
514 status_code: 204,
515 headers: HashMap::new(),
516 body: "".to_string().into(),
517 };
518 self.use_res_plugins(&mut res);
519 if let None =
520 res.headers.get("Access-Control-Allow-Methods")
521 {
522 res.headers.insert(
523 "Access-Control-Allow-Methods".to_string(),
524 allow.join(","),
525 );
526 }
527
528 return res.into();
529 }
530 };
531 }
532 }
533
534 return Self::not_found_error(message).unwrap_err().into();
535 }
536 Ok(lookup) => {
537 let upgrade = lookup.value.upgrade;
538 if self.is_query && upgrade {
539 let mut err: RawHttpResponse =
540 Self::internal_server_error().unwrap_err().into();
541 err.set_upgrade(upgrade);
542 return err;
543 }
544 let res = self
545 .build_and_execute_request(req.clone(), path, lookup, upgrade)
546 .await;
547 return res;
548 }
549 }
550 }
551 }
552 }
553}