gpui_updater_core/http/
options.rs1use std::fmt;
4
5use crate::error::{Error, Result};
6
7#[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
38pub(crate) static DEFAULT: HttpOptions = HttpOptions::new();
40
41impl HttpOptions {
42 #[must_use]
44 pub const fn new() -> Self {
45 Self {
46 headers: Vec::new(),
47 proxy: ProxyChoice::FromEnv,
48 }
49 }
50
51 #[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 #[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 #[must_use]
78 pub fn no_proxy(mut self) -> Self {
79 self.proxy = ProxyChoice::Off;
80 self
81 }
82
83 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 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 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
111impl 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}