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