Skip to main content

escouade_plugin_kit/
http.rs

1//! The calls of a plugin to its service: the HTTP client as Escouade's network settings make it, and a call whose
2//! failure says what the service said (`HttpError`).
3//!
4//! [`reqwest`] is what a plugin takes its client from, on both targets: the crate itself in process, in a module the part
5//! of it the plugins use, over the host's `net/fetch` (`fetch`).
6
7#[cfg(any(test, target_family = "wasm"))]
8pub mod fetch;
9
10#[cfg(not(target_family = "wasm"))]
11pub use ::reqwest;
12#[cfg(target_family = "wasm")]
13pub use fetch as reqwest;
14
15use self::reqwest::header::HeaderMap;
16use crate::errors::{http_error_in, HttpError};
17use crate::tr;
18use anyhow::{anyhow, Result};
19use serde_json::Value;
20use std::time::Duration;
21
22/// The network as Escouade's settings say it (Réglages › Réseau), given to a plugin of the first hand when it starts:
23/// `ESCOUADE_PROXY` (empty: none), `ESCOUADE_NO_PROXY`, `ESCOUADE_INSECURE_TLS` (`1`: the certificates are not
24/// checked, for a proxy that inspects them without its root in the system's store). A module has no environment and needs
25/// none of it: the host keeps its network.
26#[derive(Debug, Clone, Default, PartialEq)]
27pub struct Network {
28    pub proxy: String,
29    pub no_proxy: String,
30    pub insecure_tls: bool,
31}
32
33impl Network {
34    pub fn from_env() -> Self {
35        let var = |k: &str| std::env::var(k).unwrap_or_default();
36        Self {
37            proxy: var("ESCOUADE_PROXY").trim().to_string(),
38            no_proxy: var("ESCOUADE_NO_PROXY"),
39            insecure_tls: var("ESCOUADE_INSECURE_TLS").trim() == "1",
40        }
41    }
42
43    /// The client of these settings, as the core makes its own (`usage::http_client`).
44    pub fn client(&self) -> Result<reqwest::Client> {
45        let mut b = reqwest::Client::builder()
46            .timeout(Duration::from_secs(15))
47            .danger_accept_invalid_certs(self.insecure_tls);
48        if !self.proxy.is_empty() {
49            let proxy = reqwest::Proxy::all(&self.proxy)?
50                .no_proxy(reqwest::NoProxy::from_string(&self.no_proxy));
51            b = b.proxy(proxy);
52        }
53        Ok(b.build()?)
54    }
55}
56
57/// Sends a call to the service `name`: its JSON (null when empty) and its headers. A status other than a success is
58/// an `HttpError` that says what the service said.
59pub async fn send_named(name: &str, rb: reqwest::RequestBuilder) -> Result<(Value, HeaderMap)> {
60    let resp = rb.send().await.map_err(|e| {
61        let e = anyhow::Error::from(e.without_url());
62        anyhow!(tr!(
63            "{name} injoignable : {e:#}",
64            "{name} can’t be reached: {e:#}"
65        ))
66    })?;
67    let status = resp.status().as_u16();
68    let headers = resp.headers().clone();
69    let text = resp.text().await.unwrap_or_default();
70    if !(200..300).contains(&status) {
71        return Err(HttpError {
72            status,
73            message: http_error_in(crate::i18n::ui(), name, status, &text),
74        }
75        .into());
76    }
77    if text.trim().is_empty() {
78        return Ok((Value::Null, headers));
79    }
80    let v = serde_json::from_str(&text).map_err(|_| {
81        anyhow!(tr!(
82            "{name} : réponse illisible",
83            "{name}: unreadable answer"
84        ))
85    })?;
86    Ok((v, headers))
87}
88
89/// `send_named`'s JSON.
90pub async fn call(name: &str, rb: reqwest::RequestBuilder) -> Result<Value> {
91    Ok(send_named(name, rb).await?.0)
92}
93
94/// `call`, and whether the answer's `Link` header points to a next page.
95pub async fn call_page(name: &str, rb: reqwest::RequestBuilder) -> Result<(Value, bool)> {
96    let (v, headers) = send_named(name, rb).await?;
97    let next = headers
98        .get_all(reqwest::header::LINK)
99        .iter()
100        .filter_map(|h| h.to_str().ok())
101        .flat_map(|h| h.split(','))
102        .any(|link| link.contains("rel=\"next\""));
103    Ok((v, next))
104}
105
106/// Percent-encoding of everything but the unreserved characters: a path segment or a query value.
107pub fn encode(s: &str) -> String {
108    let mut out = String::new();
109    for b in s.bytes() {
110        if b.is_ascii_alphanumeric() || b"-_.~".contains(&b) {
111            out.push(b as char);
112        } else {
113            out.push_str(&format!("%{b:02X}"));
114        }
115    }
116    out
117}
118
119/// The host of an address (`https://Git.Acme.dev:8443/x` → `git.acme.dev:8443`), lowercase: the name of an instance's
120/// account, as the core's `forge::host_of` says it.
121pub fn host_of(url: &str) -> String {
122    let rest = url.split_once("://").map_or(url, |(_, r)| r);
123    rest.split('/').next().unwrap_or("").to_ascii_lowercase()
124}