Skip to main content

vynil_core/
http.rs

1use crate::{Error, Error::*, RhaiRes, rhai_err};
2use base64::{Engine as _, engine::general_purpose::STANDARD};
3use reqwest::{Certificate, Client, Response};
4use rhai::{Dynamic, Engine, Map};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::{Value, json};
8use serde_yaml;
9use tokio::runtime::Handle;
10use tracing::*;
11
12#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, JsonSchema, Default)]
13pub enum ReadMethod {
14    #[default]
15    Get,
16}
17#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, JsonSchema, Default)]
18pub enum CreateMethod {
19    #[default]
20    Post,
21    Put,
22}
23
24#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, JsonSchema, Default)]
25pub enum UpdateMethod {
26    #[default]
27    Patch,
28    Put,
29    Post,
30    None,
31}
32
33#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, JsonSchema, Default)]
34pub enum DeleteMethod {
35    #[default]
36    Delete,
37}
38
39#[derive(Clone, Debug)]
40pub struct RestClient {
41    baseurl: String,
42    headers: Map,
43    server_ca: Option<String>,
44    client_key: Option<String>,
45    client_cert: Option<String>,
46}
47
48impl RestClient {
49    #[must_use]
50    pub fn new(base: &str) -> Self {
51        Self {
52            baseurl: base.to_string(),
53            headers: Map::new(),
54            server_ca: None,
55            client_cert: None,
56            client_key: None,
57        }
58    }
59
60    pub fn baseurl(&mut self, base: &str) -> &mut RestClient {
61        self.baseurl = base.to_string();
62        self
63    }
64
65    pub fn set_server_ca(&mut self, ca: &str) {
66        self.server_ca = Some(ca.to_string());
67    }
68
69    pub fn set_mtls(&mut self, cert: &str, key: &str) {
70        self.client_cert = Some(cert.to_string());
71        self.client_key = Some(key.to_string());
72    }
73
74    pub fn baseurl_rhai(&mut self, base: String) {
75        self.baseurl(base.as_str());
76    }
77
78    pub fn headers_reset(&mut self) -> &mut RestClient {
79        self.headers = Map::new();
80        self
81    }
82
83    pub fn headers_reset_rhai(&mut self) {
84        self.headers_reset();
85    }
86
87    pub fn add_header(&mut self, key: &str, value: &str) -> &mut RestClient {
88        self.headers
89            .insert(key.to_string().into(), value.to_string().into());
90        self
91    }
92
93    pub fn add_header_rhai(&mut self, key: String, value: String) {
94        self.add_header(key.as_str(), value.as_str());
95    }
96
97    pub fn add_header_json_content(&mut self) -> &mut RestClient {
98        if self
99            .headers
100            .clone()
101            .into_iter()
102            .any(|(c, _)| c == *"Content-Type")
103        {
104            self
105        } else {
106            self.add_header("Content-Type", "application/json; charset=utf-8")
107        }
108    }
109
110    pub fn add_header_json_accept(&mut self) -> &mut RestClient {
111        for (key, val) in self.headers.clone() {
112            debug!("RestClient.header: {:} {:}", key, val);
113        }
114        if self.headers.clone().into_iter().any(|(c, _)| c == *"Accept") {
115            self
116        } else {
117            self.add_header("Accept", "application/json")
118        }
119    }
120
121    pub fn add_header_json(&mut self) {
122        self.add_header_json_content().add_header_json_accept();
123    }
124
125    pub fn add_header_bearer(&mut self, token: &str) {
126        self.add_header("Authorization", format!("Bearer {token}").as_str());
127    }
128
129    pub fn add_header_basic(&mut self, username: &str, password: &str) {
130        let hash = STANDARD.encode(format!("{username}:{password}"));
131        self.add_header("Authorization", format!("Basic {hash}").as_str());
132    }
133
134    fn get_client(&mut self) -> std::result::Result<Client, reqwest::Error> {
135        let five_sec = std::time::Duration::from_secs(60 * 5);
136        if self.server_ca.is_none() && (self.client_cert.is_none() || self.client_key.is_none()) {
137            Client::builder()
138                .user_agent(crate::get_client_name())
139                .timeout(five_sec)
140                .build()
141        } else if self.client_cert.is_none() || self.client_key.is_none() {
142            match Certificate::from_pem(self.server_ca.clone().unwrap().as_bytes()) {
143                Ok(c) => Client::builder()
144                    .user_agent(crate::get_client_name())
145                    .timeout(five_sec)
146                    .add_root_certificate(c)
147                    .use_rustls_tls()
148                    .build(),
149                Err(e) => Err(e),
150            }
151        } else {
152            let cli_cert = format!(
153                "{}\n{}",
154                self.client_key.clone().unwrap(),
155                self.client_cert.clone().unwrap()
156            );
157            match reqwest::Identity::from_pem(cli_cert.as_bytes()) {
158                Ok(identity) => {
159                    if self.server_ca.is_none() {
160                        Client::builder()
161                            .user_agent(crate::get_client_name())
162                            .timeout(five_sec)
163                            .use_rustls_tls()
164                            .identity(identity)
165                            .build()
166                    } else {
167                        match Certificate::from_pem(self.server_ca.clone().unwrap().as_bytes()) {
168                            Ok(c) => Client::builder()
169                                .user_agent(crate::get_client_name())
170                                .timeout(five_sec)
171                                .add_root_certificate(c)
172                                .use_rustls_tls()
173                                .identity(identity)
174                                .build(),
175                            Err(e) => Err(e),
176                        }
177                    }
178                }
179                Err(e) => Err(e),
180            }
181        }
182    }
183
184    pub fn http_get(&mut self, path: &str) -> std::result::Result<Response, reqwest::Error> {
185        debug!("http_get '{}' ", format!("{}/{}", self.baseurl, path));
186        match self.get_client() {
187            Ok(client) => {
188                let mut req = client.get(format!("{}/{}", self.baseurl, path));
189                for (key, val) in self.headers.clone() {
190                    req = req.header(key.to_string(), val.to_string());
191                }
192                tokio::task::block_in_place(|| Handle::current().block_on(async move { req.send().await }))
193            }
194            Err(e) => {
195                if e.is_builder() {
196                    warn!("CLIENT: {e:?}");
197                }
198                Err(e)
199            }
200        }
201    }
202
203    pub fn body_get(&mut self, path: &str) -> crate::Result<String> {
204        let response = self.http_get(path).map_err(Error::ReqwestError)?;
205        if !response.status().is_success() {
206            let status = response.status();
207            let text = tokio::task::block_in_place(|| {
208                Handle::current().block_on(async move { response.text().await })
209            })
210            .map_err(Error::ReqwestError)?;
211            return Err(Error::MethodFailed(
212                "Get".to_string(),
213                status.as_u16(),
214                format!(
215                    "The server returned the error: {} {} | {text}",
216                    status.as_str(),
217                    status.canonical_reason().unwrap_or("unknown")
218                ),
219            ));
220        }
221        let text =
222            tokio::task::block_in_place(|| Handle::current().block_on(async move { response.text().await }))
223                .map_err(Error::ReqwestError)?;
224        Ok(text)
225    }
226
227    pub fn json_get(&mut self, path: &str) -> crate::Result<Value> {
228        let text = self.body_get(path)?;
229        let json = serde_json::from_str(&text).map_err(Error::JsonError)?;
230        Ok(json)
231    }
232
233    pub fn rhai_get(&mut self, path: String) -> RhaiRes<Map> {
234        let mut ret = Map::new();
235        match self.http_get(path.as_str()) {
236            Ok(result) => {
237                ret.insert(
238                    "code".to_string().into(),
239                    Dynamic::from_int(result.status().as_u16().to_string().parse::<i64>().unwrap_or(0)),
240                );
241                tokio::task::block_in_place(|| {
242                    tokio::runtime::Handle::current().block_on(async {
243                        let headers = result
244                            .headers()
245                            .into_iter()
246                            .map(|(key, val)| {
247                                (
248                                    key.as_str().to_string(),
249                                    val.to_str().unwrap_or_default().to_string(),
250                                )
251                            })
252                            .collect::<Vec<(String, String)>>();
253                        let text = match result.text().await {
254                            Ok(t) => t,
255                            Err(e) => {
256                                ret.insert(
257                                    "body".to_string().into(),
258                                    Dynamic::from(format!("Error reading response body: {e}")),
259                                );
260                                ret.insert("json".to_string().into(), Dynamic::from(json!({})));
261                                ret.insert("headers".to_string().into(), Dynamic::from(headers));
262                                return Err(format!("Error reading response body: {e}").into());
263                            }
264                        };
265                        ret.insert(
266                            "json".to_string().into(),
267                            serde_json::from_str(&text).unwrap_or(Dynamic::from(json!({}))),
268                        );
269                        ret.insert("headers".to_string().into(), Dynamic::from(headers.clone()));
270                        ret.insert("body".to_string().into(), Dynamic::from(text));
271                        Ok(ret)
272                    })
273                })
274            }
275            Err(e) => Err(format!("{e}").into()),
276        }
277    }
278
279    pub fn http_head(&mut self, path: &str) -> std::result::Result<Response, reqwest::Error> {
280        debug!("http_head '{}' ", format!("{}/{}", self.baseurl, path));
281        match self.get_client() {
282            Ok(client) => {
283                let mut req = client.head(format!("{}/{}", self.baseurl, path));
284                for (key, val) in self.headers.clone() {
285                    req = req.header(key.to_string(), val.to_string());
286                }
287                tokio::task::block_in_place(|| Handle::current().block_on(async move { req.send().await }))
288            }
289            Err(e) => {
290                if e.is_builder() {
291                    warn!("CLIENT: {e:?}");
292                }
293                Err(e)
294            }
295        }
296    }
297
298    pub fn header_head(&mut self, path: &str) -> crate::Result<Vec<(String, String)>> {
299        let response = self.http_get(path).map_err(Error::ReqwestError)?;
300        if !response.status().is_success() {
301            let status = response.status();
302            return Err(Error::MethodFailed(
303                "Get".to_string(),
304                status.as_u16(),
305                format!(
306                    "The server returned the error: {} {}",
307                    status.as_str(),
308                    status.canonical_reason().unwrap_or("unknown")
309                ),
310            ));
311        }
312        Ok(response
313            .headers()
314            .into_iter()
315            .map(|(key, val)| {
316                (
317                    key.as_str().to_string(),
318                    val.to_str().unwrap_or_default().to_string(),
319                )
320            })
321            .collect())
322    }
323
324    pub fn rhai_head(&mut self, path: String) -> RhaiRes<Map> {
325        let mut ret = Map::new();
326        match self.http_head(path.as_str()) {
327            Ok(result) => {
328                ret.insert(
329                    "code".to_string().into(),
330                    Dynamic::from_int(result.status().as_u16().to_string().parse::<i64>().unwrap_or(0)),
331                );
332                let headers = result
333                    .headers()
334                    .into_iter()
335                    .map(|(key, val)| {
336                        (
337                            key.as_str().to_string(),
338                            val.to_str().unwrap_or_default().to_string(),
339                        )
340                    })
341                    .collect::<Vec<(String, String)>>();
342                ret.insert("headers".to_string().into(), Dynamic::from(headers.clone()));
343                Ok(ret)
344            }
345            Err(e) => Err(format!("{e}").into()),
346        }
347    }
348
349    pub fn http_patch(&mut self, path: &str, body: &str) -> crate::Result<Response> {
350        debug!("http_patch '{}' ", format!("{}/{}", self.baseurl, path));
351        match self.get_client() {
352            Ok(client) => {
353                let mut req = client
354                    .patch(format!("{}/{}", self.baseurl, path))
355                    .body(body.to_string());
356                for (key, val) in self.headers.clone() {
357                    req = req.header(key.to_string(), val.to_string());
358                }
359                tokio::task::block_in_place(|| Handle::current().block_on(async move { req.send().await }))
360                    .map_err(Error::ReqwestError)
361            }
362            Err(e) => Err(Error::ReqwestError(e)),
363        }
364    }
365
366    pub fn body_patch(&mut self, path: &str, body: &str) -> crate::Result<String> {
367        let response = self.http_patch(path, body)?;
368        if !response.status().is_success() {
369            let status = response.status();
370            let text = tokio::task::block_in_place(|| {
371                Handle::current().block_on(async move { response.text().await })
372            })
373            .map_err(Error::ReqwestError)?;
374            return Err(Error::MethodFailed(
375                "Patch".to_string(),
376                status.as_u16(),
377                format!(
378                    "The server returned the error: {} {} | {text}",
379                    status.as_str(),
380                    status.canonical_reason().unwrap_or("unknown")
381                ),
382            ));
383        }
384        let text =
385            tokio::task::block_in_place(|| Handle::current().block_on(async move { response.text().await }))
386                .map_err(Error::ReqwestError)?;
387        Ok(text)
388    }
389
390    pub fn json_patch(&mut self, path: &str, input: &Value) -> crate::Result<Value> {
391        let body = serde_json::to_string(input).map_err(Error::JsonError)?;
392        let text = self.body_patch(path, body.as_str())?;
393        let json = serde_json::from_str(&text).map_err(Error::JsonError)?;
394        Ok(json)
395    }
396
397    pub fn rhai_patch(&mut self, path: String, val: Dynamic) -> RhaiRes<Map> {
398        let body = if val.is_string() {
399            val.to_string()
400        } else {
401            match serde_json::to_string(&val) {
402                Ok(s) => s,
403                Err(e) => return Err(format!("Failed to serialize body: {e}").into()),
404            }
405        };
406        let mut ret = Map::new();
407        match self.http_patch(path.as_str(), &body) {
408            Ok(result) => {
409                ret.insert(
410                    "code".to_string().into(),
411                    Dynamic::from_int(result.status().as_u16().to_string().parse::<i64>().unwrap_or(0)),
412                );
413                tokio::task::block_in_place(|| {
414                    tokio::runtime::Handle::current().block_on(async {
415                        let headers = result
416                            .headers()
417                            .into_iter()
418                            .map(|(key, val)| {
419                                (
420                                    key.as_str().to_string(),
421                                    val.to_str().unwrap_or_default().to_string(),
422                                )
423                            })
424                            .collect::<Vec<(String, String)>>();
425                        let text = match result.text().await {
426                            Ok(t) => t,
427                            Err(e) => {
428                                ret.insert(
429                                    "body".to_string().into(),
430                                    Dynamic::from(format!("Error reading response body: {e}")),
431                                );
432                                ret.insert("json".to_string().into(), Dynamic::from(json!({})));
433                                ret.insert("headers".to_string().into(), Dynamic::from(headers));
434                                return Err(format!("Error reading response body: {e}").into());
435                            }
436                        };
437                        ret.insert(
438                            "json".to_string().into(),
439                            serde_json::from_str(&text).unwrap_or(Dynamic::from(json!({}))),
440                        );
441                        ret.insert("headers".to_string().into(), Dynamic::from(headers.clone()));
442                        ret.insert("body".to_string().into(), Dynamic::from(text));
443                        Ok(ret)
444                    })
445                })
446            }
447            Err(e) => Err(format!("{e}").into()),
448        }
449    }
450
451    pub fn http_put(&mut self, path: &str, body: &str) -> crate::Result<Response> {
452        debug!("http_put '{}' ", format!("{}/{}", self.baseurl, path));
453        match self.get_client() {
454            Ok(client) => {
455                let mut req = client
456                    .put(format!("{}/{}", self.baseurl, path))
457                    .body(body.to_string());
458                for (key, val) in self.headers.clone() {
459                    req = req.header(key.to_string(), val.to_string());
460                }
461                tokio::task::block_in_place(|| Handle::current().block_on(async move { req.send().await }))
462                    .map_err(Error::ReqwestError)
463            }
464            Err(e) => Err(Error::ReqwestError(e)),
465        }
466    }
467
468    pub fn body_put(&mut self, path: &str, body: &str) -> crate::Result<String> {
469        let response = self.http_put(path, body)?;
470        if !response.status().is_success() {
471            let status = response.status();
472            let text = tokio::task::block_in_place(|| {
473                Handle::current().block_on(async move { response.text().await })
474            })
475            .map_err(Error::ReqwestError)?;
476            return Err(Error::MethodFailed(
477                "Put".to_string(),
478                status.as_u16(),
479                format!(
480                    "The server returned the error: {} {} | {text}",
481                    status.as_str(),
482                    status.canonical_reason().unwrap_or("unknown")
483                ),
484            ));
485        }
486        let text =
487            tokio::task::block_in_place(|| Handle::current().block_on(async move { response.text().await }))
488                .map_err(Error::ReqwestError)?;
489        Ok(text)
490    }
491
492    pub fn json_put(&mut self, path: &str, input: &Value) -> crate::Result<Value> {
493        let body = serde_json::to_string(input).map_err(Error::JsonError)?;
494        let text = self.body_put(path, body.as_str())?;
495        let json = serde_json::from_str(&text).map_err(Error::JsonError)?;
496        Ok(json)
497    }
498
499    pub fn rhai_put(&mut self, path: String, val: Dynamic) -> RhaiRes<Map> {
500        let body = if val.is_string() {
501            val.to_string()
502        } else {
503            match serde_json::to_string(&val) {
504                Ok(s) => s,
505                Err(e) => return Err(format!("Failed to serialize body: {e}").into()),
506            }
507        };
508        let mut ret = Map::new();
509        match self.http_put(path.as_str(), &body) {
510            Ok(result) => {
511                ret.insert(
512                    "code".to_string().into(),
513                    Dynamic::from_int(result.status().as_u16().to_string().parse::<i64>().unwrap_or(0)),
514                );
515                tokio::task::block_in_place(|| {
516                    tokio::runtime::Handle::current().block_on(async {
517                        let headers = result
518                            .headers()
519                            .into_iter()
520                            .map(|(key, val)| {
521                                (
522                                    key.as_str().to_string(),
523                                    val.to_str().unwrap_or_default().to_string(),
524                                )
525                            })
526                            .collect::<Vec<(String, String)>>();
527                        let text = match result.text().await {
528                            Ok(t) => t,
529                            Err(e) => {
530                                ret.insert(
531                                    "body".to_string().into(),
532                                    Dynamic::from(format!("Error reading response body: {e}")),
533                                );
534                                ret.insert("json".to_string().into(), Dynamic::from(json!({})));
535                                ret.insert("headers".to_string().into(), Dynamic::from(headers));
536                                return Err(format!("Error reading response body: {e}").into());
537                            }
538                        };
539                        ret.insert(
540                            "json".to_string().into(),
541                            serde_json::from_str(&text).unwrap_or(Dynamic::from(json!({}))),
542                        );
543                        ret.insert("headers".to_string().into(), Dynamic::from(headers.clone()));
544                        ret.insert("body".to_string().into(), Dynamic::from(text));
545                        Ok(ret)
546                    })
547                })
548            }
549            Err(e) => Err(format!("{e}").into()),
550        }
551    }
552
553    pub fn http_post(&mut self, path: &str, body: &str) -> crate::Result<Response> {
554        debug!("http_post '{}' ", format!("{}/{}", self.baseurl, path));
555        match self.get_client() {
556            Ok(client) => {
557                let mut req = client
558                    .post(format!("{}/{}", self.baseurl, path))
559                    .body(body.to_string());
560                for (key, val) in self.headers.clone() {
561                    req = req.header(key.to_string(), val.to_string());
562                }
563                tokio::task::block_in_place(|| Handle::current().block_on(async move { req.send().await }))
564                    .map_err(Error::ReqwestError)
565            }
566            Err(e) => Err(Error::ReqwestError(e)),
567        }
568    }
569
570    pub fn body_post(&mut self, path: &str, body: &str) -> crate::Result<String> {
571        let response = self.http_post(path, body)?;
572        if !response.status().is_success() {
573            let status = response.status();
574            let text = tokio::task::block_in_place(|| {
575                Handle::current().block_on(async move { response.text().await })
576            })
577            .map_err(Error::ReqwestError)?;
578            return Err(Error::MethodFailed(
579                "Post".to_string(),
580                status.as_u16(),
581                format!(
582                    "The server returned the error: {} {} | {text}",
583                    status.as_str(),
584                    status.canonical_reason().unwrap_or("unknown")
585                ),
586            ));
587        }
588        let text =
589            tokio::task::block_in_place(|| Handle::current().block_on(async move { response.text().await }))
590                .map_err(Error::ReqwestError)?;
591        Ok(text)
592    }
593
594    pub fn json_post(&mut self, path: &str, input: &Value) -> crate::Result<Value> {
595        let body = serde_json::to_string(input).map_err(Error::JsonError)?;
596        let text = self.body_post(path, body.as_str())?;
597        let json = serde_json::from_str(&text).map_err(Error::JsonError)?;
598        Ok(json)
599    }
600
601    pub fn rhai_post(&mut self, path: String, val: Dynamic) -> RhaiRes<Map> {
602        let body = if val.is_string() {
603            val.to_string()
604        } else {
605            match serde_json::to_string(&val) {
606                Ok(s) => s,
607                Err(e) => return Err(format!("Failed to serialize body: {e}").into()),
608            }
609        };
610        let mut ret = Map::new();
611        match self.http_post(path.as_str(), &body) {
612            Ok(result) => {
613                ret.insert(
614                    "code".to_string().into(),
615                    Dynamic::from_int(result.status().as_u16().to_string().parse::<i64>().unwrap_or(0)),
616                );
617                tokio::task::block_in_place(|| {
618                    tokio::runtime::Handle::current().block_on(async {
619                        let headers = result
620                            .headers()
621                            .into_iter()
622                            .map(|(key, val)| {
623                                (
624                                    key.as_str().to_string(),
625                                    val.to_str().unwrap_or_default().to_string(),
626                                )
627                            })
628                            .collect::<Vec<(String, String)>>();
629                        let text = match result.text().await {
630                            Ok(t) => t,
631                            Err(e) => {
632                                ret.insert(
633                                    "body".to_string().into(),
634                                    Dynamic::from(format!("Error reading response body: {e}")),
635                                );
636                                ret.insert("json".to_string().into(), Dynamic::from(json!({})));
637                                ret.insert("headers".to_string().into(), Dynamic::from(headers));
638                                return Err(format!("Error reading response body: {e}").into());
639                            }
640                        };
641                        ret.insert(
642                            "json".to_string().into(),
643                            serde_json::from_str(&text).unwrap_or(Dynamic::from(json!({}))),
644                        );
645                        ret.insert("headers".to_string().into(), Dynamic::from(headers.clone()));
646                        ret.insert("body".to_string().into(), Dynamic::from(text));
647                        Ok(ret)
648                    })
649                })
650            }
651            Err(e) => Err(format!("{e}").into()),
652        }
653    }
654
655    pub fn http_post_form(&mut self, path: &str, params: &[(String, String)]) -> crate::Result<Response> {
656        debug!("http_post_form '{}' ", format!("{}/{}", self.baseurl, path));
657        match self.get_client() {
658            Ok(client) => {
659                let mut req = client.post(format!("{}/{}", self.baseurl, path)).form(params);
660                for (key, val) in self.headers.clone() {
661                    if key.as_str() != "Content-Type" {
662                        req = req.header(key.to_string(), val.to_string());
663                    }
664                }
665                tokio::task::block_in_place(|| Handle::current().block_on(async move { req.send().await }))
666                    .map_err(Error::ReqwestError)
667            }
668            Err(e) => Err(Error::ReqwestError(e)),
669        }
670    }
671
672    pub fn rhai_post_form(&mut self, path: String, val: Map) -> RhaiRes<Map> {
673        let params: Vec<(String, String)> = val
674            .into_iter()
675            .map(|(k, v)| (k.to_string(), v.to_string()))
676            .collect();
677        let mut ret = Map::new();
678        match self.http_post_form(path.as_str(), &params) {
679            Ok(result) => {
680                ret.insert(
681                    "code".to_string().into(),
682                    Dynamic::from_int(result.status().as_u16().to_string().parse::<i64>().unwrap_or(0)),
683                );
684                tokio::task::block_in_place(|| {
685                    tokio::runtime::Handle::current().block_on(async {
686                        let headers = result
687                            .headers()
688                            .into_iter()
689                            .map(|(key, val)| {
690                                (
691                                    key.as_str().to_string(),
692                                    val.to_str().unwrap_or_default().to_string(),
693                                )
694                            })
695                            .collect::<Vec<(String, String)>>();
696                        let text = match result.text().await {
697                            Ok(t) => t,
698                            Err(e) => {
699                                ret.insert(
700                                    "body".to_string().into(),
701                                    Dynamic::from(format!("Error reading response body: {e}")),
702                                );
703                                ret.insert("json".to_string().into(), Dynamic::from(json!({})));
704                                ret.insert("headers".to_string().into(), Dynamic::from(headers));
705                                return Err(format!("Error reading response body: {e}").into());
706                            }
707                        };
708                        ret.insert(
709                            "json".to_string().into(),
710                            serde_json::from_str(&text).unwrap_or(Dynamic::from(json!({}))),
711                        );
712                        ret.insert("headers".to_string().into(), Dynamic::from(headers.clone()));
713                        ret.insert("body".to_string().into(), Dynamic::from(text));
714                        Ok(ret)
715                    })
716                })
717            }
718            Err(e) => Err(format!("{e}").into()),
719        }
720    }
721
722    pub fn http_delete(&mut self, path: &str) -> crate::Result<Response> {
723        debug!("http_delete '{}' ", format!("{}/{}", self.baseurl, path));
724        match self.get_client() {
725            Ok(client) => {
726                let mut req = client.delete(format!("{}/{}", self.baseurl, path));
727                for (key, val) in self.headers.clone() {
728                    req = req.header(key.to_string(), val.to_string());
729                }
730                tokio::task::block_in_place(|| Handle::current().block_on(async move { req.send().await }))
731                    .map_err(Error::ReqwestError)
732            }
733            Err(e) => Err(Error::ReqwestError(e)),
734        }
735    }
736
737    pub fn body_delete(&mut self, path: &str) -> crate::Result<String> {
738        let response = self.http_delete(path)?;
739        if !response.status().is_success() && response.status() != reqwest::StatusCode::NOT_FOUND {
740            let status = response.status();
741            let text = tokio::task::block_in_place(|| {
742                Handle::current().block_on(async move { response.text().await })
743            })
744            .map_err(Error::ReqwestError)?;
745            return Err(Error::MethodFailed(
746                "Delete".to_string(),
747                status.as_u16(),
748                format!(
749                    "The server returned the error: {} {} | {text}",
750                    status.as_str(),
751                    status.canonical_reason().unwrap_or("unknown")
752                ),
753            ));
754        }
755        let text =
756            tokio::task::block_in_place(|| Handle::current().block_on(async move { response.text().await }))
757                .map_err(Error::ReqwestError)?;
758        Ok(text)
759    }
760
761    pub fn json_delete(&mut self, path: &str) -> crate::Result<Value> {
762        let text = self.body_delete(path)?;
763        let json =
764            serde_json::from_str(&text).or_else(|_| Ok::<serde_json::Value, Error>(json!({"body": text})))?;
765        Ok(json)
766    }
767
768    pub fn rhai_delete(&mut self, path: String) -> RhaiRes<Map> {
769        let mut ret = Map::new();
770        match self.http_delete(path.as_str()) {
771            Ok(result) => {
772                ret.insert(
773                    "code".to_string().into(),
774                    Dynamic::from_int(result.status().as_u16().to_string().parse::<i64>().unwrap_or(0)),
775                );
776                tokio::task::block_in_place(|| {
777                    tokio::runtime::Handle::current().block_on(async {
778                        let headers = result
779                            .headers()
780                            .into_iter()
781                            .map(|(key, val)| {
782                                (
783                                    key.as_str().to_string(),
784                                    val.to_str().unwrap_or_default().to_string(),
785                                )
786                            })
787                            .collect::<Vec<(String, String)>>();
788                        let text = match result.text().await {
789                            Ok(t) => t,
790                            Err(e) => {
791                                ret.insert(
792                                    "body".to_string().into(),
793                                    Dynamic::from(format!("Error reading response body: {e}")),
794                                );
795                                ret.insert("json".to_string().into(), Dynamic::from(json!({})));
796                                ret.insert("headers".to_string().into(), Dynamic::from(headers));
797                                return Err(format!("Error reading response body: {e}").into());
798                            }
799                        };
800                        ret.insert(
801                            "json".to_string().into(),
802                            serde_json::from_str(&text).unwrap_or(Dynamic::from(json!({}))),
803                        );
804                        ret.insert("headers".to_string().into(), Dynamic::from(headers.clone()));
805                        ret.insert("body".to_string().into(), Dynamic::from(text));
806                        Ok(ret)
807                    })
808                })
809            }
810            Err(e) => Err(format!("{e}").into()),
811        }
812    }
813
814    pub fn obj_read(&mut self, method: ReadMethod, path: &str, key: &str) -> crate::Result<Value> {
815        let full_path = if key.is_empty() {
816            path.to_string()
817        } else {
818            format!("{path}/{key}")
819        };
820        if method == ReadMethod::Get {
821            self.json_get(&full_path)
822        } else {
823            Err(UnsupportedMethod)
824        }
825    }
826
827    pub fn obj_create(&mut self, method: CreateMethod, path: &str, input: &Value) -> crate::Result<Value> {
828        if method == CreateMethod::Post {
829            self.json_post(path, input)
830        } else if method == CreateMethod::Put {
831            self.json_put(path, input)
832        } else {
833            Err(UnsupportedMethod)
834        }
835    }
836
837    pub fn obj_update(
838        &mut self,
839        method: UpdateMethod,
840        path: &str,
841        key: &str,
842        input: &Value,
843        use_slash: bool,
844    ) -> crate::Result<Value> {
845        let full_path = if key.is_empty() {
846            path.to_string()
847        } else if use_slash {
848            format!("{path}/{key}/")
849        } else {
850            format!("{path}/{key}")
851        };
852        if method == UpdateMethod::Patch {
853            self.json_patch(&full_path, input)
854        } else if method == UpdateMethod::Put {
855            self.json_put(&full_path, input)
856        } else if method == UpdateMethod::Post {
857            self.json_post(&full_path, input)
858        } else if method == UpdateMethod::None {
859            Ok(input.clone())
860        } else {
861            Err(UnsupportedMethod)
862        }
863    }
864
865    pub fn obj_delete(&mut self, method: DeleteMethod, path: &str, key: &str) -> crate::Result<Value> {
866        let full_path = if key.is_empty() {
867            path.to_string()
868        } else {
869            format!("{path}/{key}")
870        };
871        if method == DeleteMethod::Delete {
872            self.json_delete(&full_path)
873        } else {
874            Err(UnsupportedMethod)
875        }
876    }
877
878    pub fn http_delete_with_body(&mut self, path: &str, body: &str) -> crate::Result<Response> {
879        debug!(
880            "http_delete_with_body '{}' ",
881            format!("{}/{}", self.baseurl, path)
882        );
883        match self.get_client() {
884            Ok(client) => {
885                let mut req = client
886                    .delete(format!("{}/{}", self.baseurl, path))
887                    .body(body.to_string());
888                for (key, val) in self.headers.clone() {
889                    req = req.header(key.to_string(), val.to_string());
890                }
891                tokio::task::block_in_place(|| Handle::current().block_on(async move { req.send().await }))
892                    .map_err(Error::ReqwestError)
893            }
894            Err(e) => Err(Error::ReqwestError(e)),
895        }
896    }
897
898    pub fn body_delete_with_body(&mut self, path: &str, body: &str) -> crate::Result<String> {
899        let response = self.http_delete_with_body(path, body)?;
900        if !response.status().is_success() && response.status() != reqwest::StatusCode::NOT_FOUND {
901            let status = response.status();
902            let text = tokio::task::block_in_place(|| {
903                Handle::current().block_on(async move { response.text().await })
904            })
905            .map_err(Error::ReqwestError)?;
906            return Err(Error::MethodFailed(
907                "Delete".to_string(),
908                status.as_u16(),
909                format!(
910                    "The server returned the error: {} {} | {text}",
911                    status.as_str(),
912                    status.canonical_reason().unwrap_or("unknown")
913                ),
914            ));
915        }
916        let text =
917            tokio::task::block_in_place(|| Handle::current().block_on(async move { response.text().await }))
918                .map_err(Error::ReqwestError)?;
919        Ok(text)
920    }
921
922    pub fn json_delete_with_body(&mut self, path: &str, input: &Value) -> crate::Result<Value> {
923        let body = serde_json::to_string(input).map_err(Error::JsonError)?;
924        let text = self.body_delete_with_body(path, body.as_str())?;
925        let json =
926            serde_json::from_str(&text).or_else(|_| Ok::<serde_json::Value, Error>(json!({"body": text})))?;
927        Ok(json)
928    }
929
930    pub fn rhai_delete_with_body(&mut self, path: String, val: Dynamic) -> RhaiRes<Map> {
931        let body = if val.is_string() {
932            val.to_string()
933        } else {
934            match serde_json::to_string(&val) {
935                Ok(s) => s,
936                Err(e) => return Err(format!("Failed to serialize body: {e}").into()),
937            }
938        };
939        let mut ret = Map::new();
940        match self.http_delete_with_body(path.as_str(), &body) {
941            Ok(result) => {
942                ret.insert(
943                    "code".to_string().into(),
944                    Dynamic::from_int(result.status().as_u16().to_string().parse::<i64>().unwrap_or(0)),
945                );
946                tokio::task::block_in_place(|| {
947                    tokio::runtime::Handle::current().block_on(async {
948                        let headers = result
949                            .headers()
950                            .into_iter()
951                            .map(|(key, val)| {
952                                (
953                                    key.as_str().to_string(),
954                                    val.to_str().unwrap_or_default().to_string(),
955                                )
956                            })
957                            .collect::<Vec<(String, String)>>();
958                        let text = match result.text().await {
959                            Ok(t) => t,
960                            Err(e) => {
961                                ret.insert(
962                                    "body".to_string().into(),
963                                    Dynamic::from(format!("Error reading response body: {e}")),
964                                );
965                                ret.insert("json".to_string().into(), Dynamic::from(json!({})));
966                                ret.insert("headers".to_string().into(), Dynamic::from(headers));
967                                return Err(format!("Error reading response body: {e}").into());
968                            }
969                        };
970                        ret.insert(
971                            "json".to_string().into(),
972                            serde_json::from_str(&text).unwrap_or(Dynamic::from(json!({}))),
973                        );
974                        ret.insert("headers".to_string().into(), Dynamic::from(headers.clone()));
975                        ret.insert("body".to_string().into(), Dynamic::from(text));
976                        Ok(ret)
977                    })
978                })
979            }
980            Err(e) => Err(format!("{e}").into()),
981        }
982    }
983
984    pub fn obj_delete_with_body(
985        &mut self,
986        method: DeleteMethod,
987        path: &str,
988        input: &Value,
989    ) -> crate::Result<Value> {
990        if method == DeleteMethod::Delete {
991            self.json_delete_with_body(path, input)
992        } else {
993            Err(UnsupportedMethod)
994        }
995    }
996}
997
998pub fn http_get_yaml(url: String, auth_type: String, credential: String) -> RhaiRes<Dynamic> {
999    tokio::task::block_in_place(|| {
1000        Handle::current().block_on(async move {
1001            let mut headers = reqwest::header::HeaderMap::new();
1002            match auth_type.as_str() {
1003                "bearer" => {
1004                    headers.insert(
1005                        reqwest::header::AUTHORIZATION,
1006                        format!("Bearer {}", credential).parse().unwrap(),
1007                    );
1008                }
1009                "basic" => {
1010                    let encoded = STANDARD.encode(&credential);
1011                    headers.insert(
1012                        reqwest::header::AUTHORIZATION,
1013                        format!("Basic {}", encoded).parse().unwrap(),
1014                    );
1015                }
1016                _ => {}
1017            }
1018            let client = reqwest::Client::builder()
1019                .default_headers(headers)
1020                .timeout(std::time::Duration::from_secs(300))
1021                .build()
1022                .map_err(|e| Error::Other(e.to_string()))?;
1023            let response = client.get(&url).send().await.map_err(Error::ReqwestError)?;
1024            if !response.status().is_success() {
1025                return Err(Error::Other(format!(
1026                    "SCAN-HTTP-001: HTTP {} for {}",
1027                    response.status(),
1028                    url
1029                )));
1030            }
1031            let body = response.text().await.map_err(Error::ReqwestError)?;
1032            let value: serde_yaml::Value =
1033                serde_yaml::from_str(&body).map_err(|e| Error::YamlError(e.to_string()))?;
1034            let json = serde_json::to_string(&value).map_err(Error::SerializationError)?;
1035            serde_json::from_str::<Dynamic>(&json).map_err(Error::SerializationError)
1036        })
1037    })
1038    .map_err(rhai_err)
1039}
1040
1041pub fn http_rhai_register(engine: &mut Engine) {
1042    engine
1043        .register_type_with_name::<RestClient>("RestClient")
1044        .register_fn("new_http_client", RestClient::new)
1045        .register_fn("new_client", RestClient::new)
1046        .register_fn("headers_reset", RestClient::headers_reset_rhai)
1047        .register_fn("set_baseurl", RestClient::baseurl_rhai)
1048        .register_fn("set_server_ca", RestClient::set_server_ca)
1049        .register_fn("set_mtls_cert_key", RestClient::set_mtls)
1050        .register_fn("add_header", RestClient::add_header_rhai)
1051        .register_fn("add_header_json", RestClient::add_header_json)
1052        .register_fn("add_header_bearer", RestClient::add_header_bearer)
1053        .register_fn("add_header_basic", RestClient::add_header_basic)
1054        .register_fn("head", RestClient::rhai_head)
1055        .register_fn("get", RestClient::rhai_get)
1056        .register_fn("http_get", RestClient::rhai_get)
1057        .register_fn("delete", RestClient::rhai_delete)
1058        .register_fn("http_delete", RestClient::rhai_delete)
1059        .register_fn("delete_with_body", RestClient::rhai_delete_with_body)
1060        .register_fn("http_delete_with_body", RestClient::rhai_delete_with_body)
1061        .register_fn("patch", RestClient::rhai_patch)
1062        .register_fn("http_patch", RestClient::rhai_patch)
1063        .register_fn("post", RestClient::rhai_post)
1064        .register_fn("http_post", RestClient::rhai_post)
1065        .register_fn("put", RestClient::rhai_put)
1066        .register_fn("http_put", RestClient::rhai_put)
1067        .register_fn("post_form", RestClient::rhai_post_form)
1068        .register_fn("http_post_form", RestClient::rhai_post_form)
1069        .register_fn("http_get_yaml", http_get_yaml);
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074    use super::*;
1075    use wiremock::{
1076        Mock, MockServer, ResponseTemplate,
1077        matchers::{header, method, path},
1078    };
1079
1080    #[tokio::test(flavor = "multi_thread")]
1081    async fn test_http_get_yaml_ok() {
1082        let server = MockServer::start().await;
1083        Mock::given(method("GET"))
1084            .and(path("/index.yaml"))
1085            .respond_with(ResponseTemplate::new(200).set_body_string("packages:\n  - name: test\n"))
1086            .mount(&server)
1087            .await;
1088
1089        let result = http_get_yaml(
1090            format!("{}/index.yaml", server.uri()),
1091            String::new(),
1092            String::new(),
1093        );
1094        assert!(result.is_ok(), "expected Ok, got {:?}", result);
1095        let d = result.unwrap();
1096        assert!(d.is_map(), "expected map Dynamic");
1097    }
1098
1099    #[tokio::test(flavor = "multi_thread")]
1100    async fn test_http_get_yaml_404() {
1101        let server = MockServer::start().await;
1102        Mock::given(method("GET"))
1103            .and(path("/missing.yaml"))
1104            .respond_with(ResponseTemplate::new(404))
1105            .mount(&server)
1106            .await;
1107
1108        let result = http_get_yaml(
1109            format!("{}/missing.yaml", server.uri()),
1110            String::new(),
1111            String::new(),
1112        );
1113        assert!(result.is_err());
1114        let err = format!("{:?}", result.unwrap_err());
1115        assert!(
1116            err.contains("SCAN-HTTP-001"),
1117            "error should contain SCAN-HTTP-001: {err}"
1118        );
1119    }
1120
1121    #[tokio::test(flavor = "multi_thread")]
1122    async fn test_http_get_yaml_bearer() {
1123        let server = MockServer::start().await;
1124        Mock::given(method("GET"))
1125            .and(path("/index.yaml"))
1126            .and(header("authorization", "Bearer token123"))
1127            .respond_with(ResponseTemplate::new(200).set_body_string("key: value\n"))
1128            .mount(&server)
1129            .await;
1130
1131        let result = http_get_yaml(
1132            format!("{}/index.yaml", server.uri()),
1133            "bearer".to_string(),
1134            "token123".to_string(),
1135        );
1136        assert!(result.is_ok(), "expected Ok, got {:?}", result);
1137    }
1138
1139    #[tokio::test(flavor = "multi_thread")]
1140    async fn test_http_get_yaml_basic() {
1141        let server = MockServer::start().await;
1142        Mock::given(method("GET"))
1143            .and(path("/index.yaml"))
1144            .and(header("authorization", "Basic dXNlcjpwYXNz"))
1145            .respond_with(ResponseTemplate::new(200).set_body_string("key: value\n"))
1146            .mount(&server)
1147            .await;
1148
1149        let result = http_get_yaml(
1150            format!("{}/index.yaml", server.uri()),
1151            "basic".to_string(),
1152            "user:pass".to_string(),
1153        );
1154        assert!(result.is_ok(), "expected Ok, got {:?}", result);
1155    }
1156}