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}