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;
10pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
11use http::{HeaderName, HeaderValue};
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
59#[derive(Default, Clone, Debug)]
65pub struct CustomHeaders(Arc<[(HeaderName, HeaderValue)]>);
66
67impl CustomHeaders {
68 pub fn new(headers: Vec<(HeaderName, HeaderValue)>) -> Self {
69 Self(headers.into())
70 }
71
72 pub fn is_empty(&self) -> bool {
73 self.0.is_empty()
74 }
75
76 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&HeaderName, &HeaderValue)> {
77 self.0.iter().map(|(n, v)| (n, v))
78 }
79}
80
81impl PartialEq for CustomHeaders {
82 fn eq(&self, other: &Self) -> bool {
83 self.0.len() == other.0.len()
84 && self
85 .0
86 .iter()
87 .zip(other.0.iter())
88 .all(|(a, b)| a.0 == b.0 && a.1 == b.1)
89 }
90}
91
92pub trait RequestBuilderExt {
93 fn extra_headers(self, headers: &CustomHeaders) -> Self;
95}
96
97impl RequestBuilderExt for http::request::Builder {
98 fn extra_headers(mut self, headers: &CustomHeaders) -> Self {
99 if headers.is_empty() {
100 return self;
101 }
102 if let Some(map) = self.headers_mut() {
103 for (name, value) in headers.iter() {
104 map.append(name.clone(), value.clone());
105 }
106 }
107 self
108 }
109}
110
111pub trait HttpClient: 'static + Send + Sync {
112 fn user_agent(&self) -> Option<&HeaderValue>;
113
114 fn proxy(&self) -> Option<&Url>;
115
116 fn send(
117 &self,
118 req: http::Request<AsyncBody>,
119 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
120
121 fn get(
122 &self,
123 uri: &str,
124 body: AsyncBody,
125 follow_redirects: bool,
126 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
127 let request = Builder::new()
128 .uri(uri)
129 .follow_redirects(if follow_redirects {
130 RedirectPolicy::FollowAll
131 } else {
132 RedirectPolicy::NoFollow
133 })
134 .body(body);
135
136 match request {
137 Ok(request) => self.send(request),
138 Err(e) => Box::pin(async move { Err(e.into()) }),
139 }
140 }
141
142 fn post_json(
143 &self,
144 uri: &str,
145 body: AsyncBody,
146 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
147 let request = Builder::new()
148 .uri(uri)
149 .method(Method::POST)
150 .header("Content-Type", "application/json")
151 .body(body);
152
153 match request {
154 Ok(request) => self.send(request),
155 Err(e) => Box::pin(async move { Err(e.into()) }),
156 }
157 }
158
159 #[cfg(feature = "test-support")]
160 fn as_fake(&self) -> &FakeHttpClient {
161 panic!("called as_fake on {}", type_name::<Self>())
162 }
163}
164
165#[derive(Deref)]
167pub struct HttpClientWithProxy {
168 #[deref]
169 client: Arc<dyn HttpClient>,
170 proxy: Option<Url>,
171}
172
173impl HttpClientWithProxy {
174 pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
176 let proxy_url = proxy_url
177 .and_then(|proxy| proxy.parse().ok())
178 .or_else(read_proxy_from_env);
179
180 Self::new_url(client, proxy_url)
181 }
182 pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
183 Self {
184 client,
185 proxy: proxy_url,
186 }
187 }
188}
189
190impl HttpClient for HttpClientWithProxy {
191 fn send(
192 &self,
193 req: Request<AsyncBody>,
194 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
195 self.client.send(req)
196 }
197
198 fn user_agent(&self) -> Option<&HeaderValue> {
199 self.client.user_agent()
200 }
201
202 fn proxy(&self) -> Option<&Url> {
203 self.proxy.as_ref()
204 }
205
206 #[cfg(feature = "test-support")]
207 fn as_fake(&self) -> &FakeHttpClient {
208 self.client.as_fake()
209 }
210}
211
212#[derive(Deref)]
214pub struct HttpClientWithUrl {
215 base_url: Mutex<String>,
216 #[deref]
217 client: HttpClientWithProxy,
218}
219
220impl HttpClientWithUrl {
221 pub fn new(
223 client: Arc<dyn HttpClient>,
224 base_url: impl Into<String>,
225 proxy_url: Option<String>,
226 ) -> Self {
227 let client = HttpClientWithProxy::new(client, proxy_url);
228
229 Self {
230 base_url: Mutex::new(base_url.into()),
231 client,
232 }
233 }
234
235 pub fn new_url(
236 client: Arc<dyn HttpClient>,
237 base_url: impl Into<String>,
238 proxy_url: Option<Url>,
239 ) -> Self {
240 let client = HttpClientWithProxy::new_url(client, proxy_url);
241
242 Self {
243 base_url: Mutex::new(base_url.into()),
244 client,
245 }
246 }
247
248 pub fn base_url(&self) -> String {
250 self.base_url.lock().clone()
251 }
252
253 pub fn set_base_url(&self, base_url: impl Into<String>) {
255 let base_url = base_url.into();
256 *self.base_url.lock() = base_url;
257 }
258
259 pub fn build_url(&self, path: &str) -> String {
261 format!("{}{}", self.base_url(), path)
262 }
263}
264
265impl HttpClient for HttpClientWithUrl {
266 fn send(
267 &self,
268 req: Request<AsyncBody>,
269 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
270 self.client.send(req)
271 }
272
273 fn user_agent(&self) -> Option<&HeaderValue> {
274 self.client.user_agent()
275 }
276
277 fn proxy(&self) -> Option<&Url> {
278 self.client.proxy.as_ref()
279 }
280
281 #[cfg(feature = "test-support")]
282 fn as_fake(&self) -> &FakeHttpClient {
283 self.client.as_fake()
284 }
285}
286
287pub fn read_proxy_from_env() -> Option<Url> {
288 const ENV_VARS: &[&str] = &[
289 "ALL_PROXY",
290 "all_proxy",
291 "HTTPS_PROXY",
292 "https_proxy",
293 "HTTP_PROXY",
294 "http_proxy",
295 ];
296
297 ENV_VARS
298 .iter()
299 .find_map(|var| std::env::var(var).ok())
300 .and_then(|env| env.parse().ok())
301}
302
303pub fn read_no_proxy_from_env() -> Option<String> {
304 const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
305
306 ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
307}
308
309pub struct BlockedHttpClient;
310
311impl BlockedHttpClient {
312 pub fn new() -> Self {
313 BlockedHttpClient
314 }
315}
316
317impl HttpClient for BlockedHttpClient {
318 fn send(
319 &self,
320 _req: Request<AsyncBody>,
321 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
322 Box::pin(async {
323 Err(std::io::Error::new(
324 std::io::ErrorKind::PermissionDenied,
325 "BlockedHttpClient disallowed request",
326 )
327 .into())
328 })
329 }
330
331 fn user_agent(&self) -> Option<&HeaderValue> {
332 None
333 }
334
335 fn proxy(&self) -> Option<&Url> {
336 None
337 }
338
339 #[cfg(feature = "test-support")]
340 fn as_fake(&self) -> &FakeHttpClient {
341 panic!("called as_fake on {}", type_name::<Self>())
342 }
343}
344
345#[cfg(feature = "test-support")]
346type FakeHttpHandler = Arc<
347 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
348 + Send
349 + Sync
350 + 'static,
351>;
352
353#[cfg(feature = "test-support")]
354pub struct FakeHttpClient {
355 handler: Mutex<Option<FakeHttpHandler>>,
356 user_agent: HeaderValue,
357}
358
359#[cfg(feature = "test-support")]
360impl FakeHttpClient {
361 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
362 where
363 Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
364 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
365 {
366 Arc::new(HttpClientWithUrl {
367 base_url: Mutex::new("http://test.example".into()),
368 client: HttpClientWithProxy {
369 client: Arc::new(Self {
370 handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
371 user_agent: HeaderValue::from_static(type_name::<Self>()),
372 }),
373 proxy: None,
374 },
375 })
376 }
377
378 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
379 log::warn!("Using fake HTTP client with 404 response");
380 Self::create(|_| async move {
381 Ok(Response::builder()
382 .status(404)
383 .body(Default::default())
384 .unwrap())
385 })
386 }
387
388 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
389 log::warn!("Using fake HTTP client with 200 response");
390 Self::create(|_| async move {
391 Ok(Response::builder()
392 .status(200)
393 .body(Default::default())
394 .unwrap())
395 })
396 }
397
398 pub fn replace_handler<Fut, F>(&self, new_handler: F)
399 where
400 Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
401 F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
402 {
403 let mut handler = self.handler.lock();
404 let old_handler = handler.take().unwrap();
405 *handler = Some(Arc::new(move |req| {
406 Box::pin(new_handler(old_handler.clone(), req))
407 }));
408 }
409}
410
411#[cfg(feature = "test-support")]
412impl fmt::Debug for FakeHttpClient {
413 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414 f.debug_struct("FakeHttpClient").finish()
415 }
416}
417
418#[cfg(feature = "test-support")]
419impl HttpClient for FakeHttpClient {
420 fn send(
421 &self,
422 req: Request<AsyncBody>,
423 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
424 ((self.handler.lock().as_ref().unwrap())(req)) as _
425 }
426
427 fn user_agent(&self) -> Option<&HeaderValue> {
428 Some(&self.user_agent)
429 }
430
431 fn proxy(&self) -> Option<&Url> {
432 None
433 }
434
435 fn as_fake(&self) -> &FakeHttpClient {
436 self
437 }
438}