1mod async_body;
2#[cfg(not(target_family = "wasm"))]
3pub mod github;
4#[cfg(not(target_family = "wasm"))]
5pub mod github_download;
6
7pub use anyhow::{Result, anyhow};
8pub use async_body::{AsyncBody, Inner, Json};
9use derive_more::Deref;
10use http::HeaderValue;
11pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
12
13use futures::future::BoxFuture;
14use parking_lot::Mutex;
15use std::sync::Arc;
16#[cfg(feature = "test-support")]
17use std::{any::type_name, fmt};
18pub use url::{Host, Url};
19
20#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
21pub enum RedirectPolicy {
22 #[default]
23 NoFollow,
24 FollowLimit(u32),
25 FollowAll,
26}
27pub struct FollowRedirects(pub bool);
28
29pub trait HttpRequestExt {
30 fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
32 where
33 Self: Sized,
34 {
35 if condition { then(self) } else { self }
36 }
37
38 fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
40 where
41 Self: Sized,
42 {
43 match option {
44 Some(value) => then(self, value),
45 None => self,
46 }
47 }
48
49 fn follow_redirects(self, follow: RedirectPolicy) -> Self;
51}
52
53impl HttpRequestExt for http::request::Builder {
54 fn follow_redirects(self, follow: RedirectPolicy) -> Self {
55 self.extension(follow)
56 }
57}
58
59pub trait HttpClient: 'static + Send + Sync {
60 fn user_agent(&self) -> Option<&HeaderValue>;
61
62 fn proxy(&self) -> Option<&Url>;
63
64 fn send(
65 &self,
66 req: http::Request<AsyncBody>,
67 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
68
69 fn get(
70 &self,
71 uri: &str,
72 body: AsyncBody,
73 follow_redirects: bool,
74 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
75 let request = Builder::new()
76 .uri(uri)
77 .follow_redirects(if follow_redirects {
78 RedirectPolicy::FollowAll
79 } else {
80 RedirectPolicy::NoFollow
81 })
82 .body(body);
83
84 match request {
85 Ok(request) => self.send(request),
86 Err(e) => Box::pin(async move { Err(e.into()) }),
87 }
88 }
89
90 fn post_json(
91 &self,
92 uri: &str,
93 body: AsyncBody,
94 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
95 let request = Builder::new()
96 .uri(uri)
97 .method(Method::POST)
98 .header("Content-Type", "application/json")
99 .body(body);
100
101 match request {
102 Ok(request) => self.send(request),
103 Err(e) => Box::pin(async move { Err(e.into()) }),
104 }
105 }
106
107 #[cfg(feature = "test-support")]
108 fn as_fake(&self) -> &FakeHttpClient {
109 panic!("called as_fake on {}", type_name::<Self>())
110 }
111}
112
113#[derive(Deref)]
115pub struct HttpClientWithProxy {
116 #[deref]
117 client: Arc<dyn HttpClient>,
118 proxy: Option<Url>,
119}
120
121impl HttpClientWithProxy {
122 pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
124 let proxy_url = proxy_url
125 .and_then(|proxy| proxy.parse().ok())
126 .or_else(read_proxy_from_env);
127
128 Self::new_url(client, proxy_url)
129 }
130 pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
131 Self {
132 client,
133 proxy: proxy_url,
134 }
135 }
136}
137
138impl HttpClient for HttpClientWithProxy {
139 fn send(
140 &self,
141 req: Request<AsyncBody>,
142 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
143 self.client.send(req)
144 }
145
146 fn user_agent(&self) -> Option<&HeaderValue> {
147 self.client.user_agent()
148 }
149
150 fn proxy(&self) -> Option<&Url> {
151 self.proxy.as_ref()
152 }
153
154 #[cfg(feature = "test-support")]
155 fn as_fake(&self) -> &FakeHttpClient {
156 self.client.as_fake()
157 }
158}
159
160#[derive(Deref)]
162pub struct HttpClientWithUrl {
163 base_url: Mutex<String>,
164 #[deref]
165 client: HttpClientWithProxy,
166}
167
168impl HttpClientWithUrl {
169 pub fn new(
171 client: Arc<dyn HttpClient>,
172 base_url: impl Into<String>,
173 proxy_url: Option<String>,
174 ) -> Self {
175 let client = HttpClientWithProxy::new(client, proxy_url);
176
177 Self {
178 base_url: Mutex::new(base_url.into()),
179 client,
180 }
181 }
182
183 pub fn new_url(
184 client: Arc<dyn HttpClient>,
185 base_url: impl Into<String>,
186 proxy_url: Option<Url>,
187 ) -> Self {
188 let client = HttpClientWithProxy::new_url(client, proxy_url);
189
190 Self {
191 base_url: Mutex::new(base_url.into()),
192 client,
193 }
194 }
195
196 pub fn base_url(&self) -> String {
198 self.base_url.lock().clone()
199 }
200
201 pub fn set_base_url(&self, base_url: impl Into<String>) {
203 let base_url = base_url.into();
204 *self.base_url.lock() = base_url;
205 }
206
207 pub fn build_url(&self, path: &str) -> String {
209 format!("{}{}", self.base_url(), path)
210 }
211}
212
213impl HttpClient for HttpClientWithUrl {
214 fn send(
215 &self,
216 req: Request<AsyncBody>,
217 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
218 self.client.send(req)
219 }
220
221 fn user_agent(&self) -> Option<&HeaderValue> {
222 self.client.user_agent()
223 }
224
225 fn proxy(&self) -> Option<&Url> {
226 self.client.proxy.as_ref()
227 }
228
229 #[cfg(feature = "test-support")]
230 fn as_fake(&self) -> &FakeHttpClient {
231 self.client.as_fake()
232 }
233}
234
235pub fn read_proxy_from_env() -> Option<Url> {
236 const ENV_VARS: &[&str] = &[
237 "ALL_PROXY",
238 "all_proxy",
239 "HTTPS_PROXY",
240 "https_proxy",
241 "HTTP_PROXY",
242 "http_proxy",
243 ];
244
245 ENV_VARS
246 .iter()
247 .find_map(|var| std::env::var(var).ok())
248 .and_then(|env| env.parse().ok())
249}
250
251pub fn read_no_proxy_from_env() -> Option<String> {
252 const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
253
254 ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
255}
256
257pub struct BlockedHttpClient;
258
259impl BlockedHttpClient {
260 pub fn new() -> Self {
261 BlockedHttpClient
262 }
263}
264
265impl HttpClient for BlockedHttpClient {
266 fn send(
267 &self,
268 _req: Request<AsyncBody>,
269 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
270 Box::pin(async {
271 Err(std::io::Error::new(
272 std::io::ErrorKind::PermissionDenied,
273 "BlockedHttpClient disallowed request",
274 )
275 .into())
276 })
277 }
278
279 fn user_agent(&self) -> Option<&HeaderValue> {
280 None
281 }
282
283 fn proxy(&self) -> Option<&Url> {
284 None
285 }
286
287 #[cfg(feature = "test-support")]
288 fn as_fake(&self) -> &FakeHttpClient {
289 panic!("called as_fake on {}", type_name::<Self>())
290 }
291}
292
293#[cfg(feature = "test-support")]
294type FakeHttpHandler = Arc<
295 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
296 + Send
297 + Sync
298 + 'static,
299>;
300
301#[cfg(feature = "test-support")]
302pub struct FakeHttpClient {
303 handler: Mutex<Option<FakeHttpHandler>>,
304 user_agent: HeaderValue,
305}
306
307#[cfg(feature = "test-support")]
308impl FakeHttpClient {
309 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
310 where
311 Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
312 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
313 {
314 Arc::new(HttpClientWithUrl {
315 base_url: Mutex::new("http://test.example".into()),
316 client: HttpClientWithProxy {
317 client: Arc::new(Self {
318 handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
319 user_agent: HeaderValue::from_static(type_name::<Self>()),
320 }),
321 proxy: None,
322 },
323 })
324 }
325
326 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
327 log::warn!("Using fake HTTP client with 404 response");
328 Self::create(|_| async move {
329 Ok(Response::builder()
330 .status(404)
331 .body(Default::default())
332 .unwrap())
333 })
334 }
335
336 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
337 log::warn!("Using fake HTTP client with 200 response");
338 Self::create(|_| async move {
339 Ok(Response::builder()
340 .status(200)
341 .body(Default::default())
342 .unwrap())
343 })
344 }
345
346 pub fn replace_handler<Fut, F>(&self, new_handler: F)
347 where
348 Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
349 F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
350 {
351 let mut handler = self.handler.lock();
352 let old_handler = handler.take().unwrap();
353 *handler = Some(Arc::new(move |req| {
354 Box::pin(new_handler(old_handler.clone(), req))
355 }));
356 }
357}
358
359#[cfg(feature = "test-support")]
360impl fmt::Debug for FakeHttpClient {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 f.debug_struct("FakeHttpClient").finish()
363 }
364}
365
366#[cfg(feature = "test-support")]
367impl HttpClient for FakeHttpClient {
368 fn send(
369 &self,
370 req: Request<AsyncBody>,
371 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
372 ((self.handler.lock().as_ref().unwrap())(req)) as _
373 }
374
375 fn user_agent(&self) -> Option<&HeaderValue> {
376 Some(&self.user_agent)
377 }
378
379 fn proxy(&self) -> Option<&Url> {
380 None
381 }
382
383 fn as_fake(&self) -> &FakeHttpClient {
384 self
385 }
386}