Skip to main content

gpui_updater_core/http/
options.rs

1//! [`HttpOptions`]: request headers and the proxy for update traffic.
2
3use std::fmt;
4
5use crate::error::{Error, Result};
6
7/// HTTP settings for an update source or for the engine's downloads: extra
8/// request headers and the proxy.
9///
10/// By default no header is added and the proxy comes from the environment
11/// (`ALL_PROXY`, `HTTPS_PROXY`, `HTTP_PROXY`, and their lowercase forms),
12/// as before. Pass the options to the source
13/// (for example [`StaticManifestSource::http`](crate::StaticManifestSource::http))
14/// for metadata requests, and to [`EngineConfig::http`](crate::EngineConfig::http)
15/// for artifacts, deltas, and their checksum and signature files.
16///
17/// ```
18/// use gpui_updater_core::HttpOptions;
19///
20/// let http = HttpOptions::new()
21///     .header("authorization", "Bearer s3cr3t")
22///     .proxy("http://proxy.internal:3128");
23/// ```
24#[derive(Clone, Default)]
25pub struct HttpOptions {
26    headers: Vec<(String, String)>,
27    proxy: ProxyChoice,
28}
29
30#[derive(Clone, Default)]
31enum ProxyChoice {
32    #[default]
33    FromEnv,
34    Off,
35    Url(String),
36}
37
38/// Options with nothing set, for callers that have none.
39pub(crate) static DEFAULT: HttpOptions = HttpOptions::new();
40
41impl HttpOptions {
42    /// No extra headers, and the proxy from the environment.
43    #[must_use]
44    pub const fn new() -> Self {
45        Self {
46            headers: Vec::new(),
47            proxy: ProxyChoice::FromEnv,
48        }
49    }
50
51    /// Send `name: value` with every request.
52    ///
53    /// Put a credential in `authorization`: it is dropped when a request is
54    /// redirected, so a release asset's storage host never sees it. Every
55    /// other header goes to redirect targets as well. A header the source
56    /// sets itself, such as the `authorization` from
57    /// [`GitHubSource::token`](crate::GitHubSource::token), takes precedence
58    /// over one set here. An invalid name or value fails the request.
59    #[must_use]
60    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
61        self.headers.push((name.into(), value.into()));
62        self
63    }
64
65    /// Send requests through the proxy at `url` instead of the one from the
66    /// environment. The scheme is `http`, `https`, `socks4`, `socks4a`, or
67    /// `socks5`; credentials go in the URL (`http://user:pass@host:port`).
68    /// HTTPS requests are tunnelled, so TLS still ends at the update server.
69    /// An invalid URL fails every request.
70    #[must_use]
71    pub fn proxy(mut self, url: impl Into<String>) -> Self {
72        self.proxy = ProxyChoice::Url(url.into());
73        self
74    }
75
76    /// Connect directly, ignoring the proxy environment variables.
77    #[must_use]
78    pub fn no_proxy(mut self) -> Self {
79        self.proxy = ProxyChoice::Off;
80        self
81    }
82
83    /// The proxy for an agent.
84    pub(crate) fn resolve_proxy(&self) -> Result<Option<ureq::Proxy>> {
85        match &self.proxy {
86            ProxyChoice::FromEnv => Ok(ureq::Proxy::try_from_env()),
87            ProxyChoice::Off => Ok(None),
88            // The URL may hold credentials, so the error does not repeat it.
89            ProxyChoice::Url(url) => ureq::Proxy::new(url)
90                .map(Some)
91                .map_err(|e| Error::Http(format!("invalid proxy URL: {e}"))),
92        }
93    }
94
95    /// `own` followed by the headers set here that `own` does not already
96    /// name.
97    pub(crate) fn merge(&self, own: &[(&str, &str)]) -> Vec<(String, String)> {
98        let mut merged: Vec<(String, String)> = own
99            .iter()
100            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
101            .collect();
102        for (name, value) in &self.headers {
103            if !own.iter().any(|(k, _)| k.eq_ignore_ascii_case(name)) {
104                merged.push((name.clone(), value.clone()));
105            }
106        }
107        merged
108    }
109}
110
111/// Shows header names and whether a proxy is set, never header values or the
112/// proxy URL, which may hold credentials.
113impl fmt::Debug for HttpOptions {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        let names: Vec<&str> = self.headers.iter().map(|(k, _)| k.as_str()).collect();
116        let proxy = match self.proxy {
117            ProxyChoice::FromEnv => "from environment",
118            ProxyChoice::Off => "none",
119            ProxyChoice::Url(_) => "configured",
120        };
121        f.debug_struct("HttpOptions")
122            .field("headers", &names)
123            .field("proxy", &proxy)
124            .finish()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn own_headers_win_over_configured_ones() {
134        let http = HttpOptions::new()
135            .header("Authorization", "Bearer mine")
136            .header("x-team", "desktop");
137        let merged = http.merge(&[("authorization", "Bearer source")]);
138        assert_eq!(
139            merged,
140            [
141                ("authorization".to_string(), "Bearer source".to_string()),
142                ("x-team".to_string(), "desktop".to_string()),
143            ]
144        );
145    }
146
147    #[test]
148    fn debug_hides_header_values_and_the_proxy_url() {
149        let http = HttpOptions::new()
150            .header("authorization", "Bearer s3cr3t")
151            .proxy("http://user:hunter2@proxy:3128");
152        let shown = format!("{http:?}");
153        assert!(shown.contains("authorization"), "{shown}");
154        assert!(
155            !shown.contains("s3cr3t") && !shown.contains("hunter2"),
156            "{shown}"
157        );
158    }
159
160    #[test]
161    fn rejects_an_invalid_proxy_url_without_repeating_it() {
162        let err = HttpOptions::new()
163            .proxy("ftp://user:hunter2@proxy")
164            .resolve_proxy()
165            .unwrap_err();
166        assert!(!err.to_string().contains("hunter2"), "{err}");
167        assert!(
168            HttpOptions::new()
169                .no_proxy()
170                .resolve_proxy()
171                .unwrap()
172                .is_none()
173        );
174    }
175}