1#[cfg(not(target_arch = "wasm32"))]
22mod h1;
23
24use blitz_traits::net::{
25 AbortSignal, Body, Bytes, CookieJar as CookieStorage, NetHandler, NetProvider, NetWaker, Request,
26 http::{HeaderValue, header::CONTENT_TYPE},
27};
28use blitz_traits::platform::{
29 FetchError, FetchHandler, FetchProvider, FetchRequest, FetchResponse, HeaderMap, StatusCode,
30};
31use data_url::DataUrl;
32use std::{marker::PhantomData, pin::Pin, sync::Arc, task::Poll};
33
34#[cfg(feature = "cookies")]
40pub use blitz_traits::net::CookieJar;
41
42#[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
43pub use h1::cache::PrivateCache;
44
45pub const DEFAULT_USER_AGENT: &str =
54 "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0";
55
56#[cfg(not(target_arch = "wasm32"))]
57type Client = h1::H1Client;
58
59#[cfg(target_arch = "wasm32")]
60#[derive(Clone)]
61struct Client;
62
63#[cfg(target_arch = "wasm32")]
64impl Client {
65 fn new(
66 user_agent: &str,
67 _cookies: Option<Arc<dyn CookieStorage>>,
68 ) -> Result<Self, FetchError> {
69 HeaderValue::from_str(user_agent)
70 .map_err(|error| FetchError::InvalidRequest(error.to_string()))?;
71 Ok(Self)
72 }
73
74 async fn fetch(&self, _request: FetchRequest) -> Result<FetchResponse, FetchError> {
75 Err(FetchError::Network(
76 "the native HTTP transport is unavailable on wasm32".to_owned(),
77 ))
78 }
79}
80
81#[cfg(target_arch = "wasm32")]
82fn spawn(fut: impl Future + 'static) {
83 wasm_bindgen_futures::spawn_local(async move {
84 fut.await;
85 });
86}
87
88#[cfg(not(target_arch = "wasm32"))]
89fn spawn<F>(fut: F)
90where
91 F: Future + Send + 'static,
92 F::Output: Send + 'static,
93{
94 nagoya::spawn(fut);
95}
96
97pub struct Provider {
103 client: Client,
104 waker: Arc<dyn NetWaker>,
105 user_agent: Arc<str>,
106 #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
107 cache_manager: Option<Arc<PrivateCache>>,
108}
109impl Provider {
110 pub fn new(waker: Option<Arc<dyn NetWaker>>) -> Self {
111 Self::with_user_agent(waker, DEFAULT_USER_AGENT)
112 }
113
114 pub fn with_user_agent(waker: Option<Arc<dyn NetWaker>>, user_agent: &str) -> Self {
122 Self::with_cookie_jar(waker, user_agent, None)
123 }
124
125 #[cfg(feature = "cookies")]
130 pub fn with_user_agent_and_cookie_provider<C>(
131 waker: Option<Arc<dyn NetWaker>>,
132 user_agent: &str,
133 cookie_provider: Arc<C>,
134 ) -> Self
135 where
136 C: CookieJar,
137 {
138 Self::with_cookie_jar(waker, user_agent, Some(cookie_provider))
139 }
140
141 fn with_cookie_jar(
142 waker: Option<Arc<dyn NetWaker>>,
143 user_agent: &str,
144 cookies: Option<Arc<dyn CookieStorage>>,
145 ) -> Self {
146 let client = Client::new(user_agent, cookies)
147 .expect("failed to initialize the house HTTP client");
148 let waker = waker.unwrap_or(Arc::new(DummyNetWaker));
149 Self {
150 client,
151 waker,
152 user_agent: Arc::from(user_agent),
153 #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
154 cache_manager: None,
155 }
156 }
157
158 #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
163 pub fn with_cache(mut self, cache: Arc<PrivateCache>) -> Self {
164 self.client = self.client.with_cache(cache.clone());
165 self.cache_manager = Some(cache);
166 self
167 }
168
169 #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
174 pub async fn with_cache_directory(
175 self,
176 profile_directory: impl AsRef<std::path::Path>,
177 ) -> std::io::Result<Self> {
178 let directory = profile_directory.as_ref().to_owned();
179 let cache = nagoya::spawn(async move { PrivateCache::open(directory).await })
180 .await
181 .ok_or_else(|| std::io::Error::other("cache opener was cancelled"))??;
182 Ok(self.with_cache(cache))
183 }
184
185 pub fn shared(waker: Option<Arc<dyn NetWaker>>) -> Arc<dyn NetProvider> {
186 Arc::new(Self::new(waker))
187 }
188 pub fn shared_with_user_agent(
189 waker: Option<Arc<dyn NetWaker>>,
190 user_agent: &str,
191 ) -> Arc<dyn NetProvider> {
192 Arc::new(Self::with_user_agent(waker, user_agent))
193 }
194 pub fn user_agent(&self) -> &str {
196 &self.user_agent
197 }
198 pub fn is_empty(&self) -> bool {
199 Arc::strong_count(&self.waker) == 1
200 }
201 pub fn count(&self) -> usize {
202 Arc::strong_count(&self.waker) - 1
203 }
204
205 #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
206 pub async fn clear_cache(&self) {
207 if let Some(cache) = &self.cache_manager {
208 let cache = cache.clone();
209 let result = nagoya::spawn(async move { cache.clear().await }).await;
210 if let Some(Err(e)) = result {
211 #[cfg(feature = "tracing")]
212 tracing::error!("Failed to clear HTTP cache: {:?}", e);
213 #[cfg(not(feature = "tracing"))]
214 let _ = e;
215 }
216 }
217 }
218}
219impl Provider {
220 async fn fetch_inner(
221 client: Client,
222 request: Request,
223 ) -> Result<(String, Bytes), ProviderError> {
224 let response = Self::fetch_response_inner(client, request).await?;
225 Ok((response.url.to_string(), response.body))
226 }
227
228 #[allow(clippy::type_complexity)]
229 pub fn fetch_with_callback(
230 &self,
231 request: Request,
232 callback: Box<dyn FnOnce(Result<(String, Bytes), ProviderError>) + Send + Sync + 'static>,
233 ) {
234 #[cfg(feature = "tracing")]
235 let url = request.url.to_string();
236
237 let client = self.client.clone();
238 spawn(async move {
239 let result = Self::fetch_inner(client, request).await;
240
241 #[cfg(feature = "tracing")]
242 if let Err(e) = &result {
243 tracing::error!(url = url.as_str(), error = ?e, "Fetching");
244 } else {
245 tracing::info!(url = url.as_str(), "Success fetching");
246 }
247
248 callback(result);
249 });
250 }
251
252 pub async fn fetch_async(&self, request: Request) -> Result<(String, Bytes), ProviderError> {
253 #[cfg(feature = "tracing")]
254 let url = request.url.to_string();
255
256 let result = Self::fetch_inner(self.client.clone(), request).await;
257
258 #[cfg(feature = "tracing")]
259 if let Err(e) = &result {
260 tracing::error!(url = url.as_str(), error = ?e, "Fetching");
261 } else {
262 tracing::info!(url = url.as_str(), "Success fetching");
263 }
264
265 result
266 }
267
268 pub async fn fetch_response_async(
275 &self,
276 request: Request,
277 ) -> Result<FetchResponse, ProviderError> {
278 Self::fetch_response_inner(self.client.clone(), request).await
279 }
280
281 async fn fetch_response_inner(
282 client: Client,
283 request: Request,
284 ) -> Result<FetchResponse, ProviderError> {
285 let url = request.url.clone();
286 match url.scheme() {
287 "data" => {
288 let (body, headers) = {
291 let data_url = DataUrl::process(url.as_str())?;
292 let decoded = data_url.decode_to_vec()?;
293 let mut headers = HeaderMap::new();
294 if let Ok(value) = data_url.mime_type().to_string().parse() {
295 headers.insert(CONTENT_TYPE, value);
296 }
297 (Bytes::from(decoded.0), headers)
298 };
299 Ok(FetchResponse::new(url, StatusCode::OK)
300 .headers(headers)
301 .body(body))
302 }
303 "file" => {
304 let file_content = std::fs::read(url.path())?;
305 Ok(FetchResponse::new(url, StatusCode::OK).body(Bytes::from(file_content)))
306 }
307 "http" | "https" => {
308 let mut wire_request = FetchRequest::get(request.url).method(request.method);
309 wire_request.headers = request.headers;
310 let content_type = request.content_type.or_else(|| {
311 wire_request
312 .headers
313 .get(CONTENT_TYPE)
314 .and_then(|value| value.to_str().ok())
315 .map(str::to_owned)
316 });
317 if let Some(content_type) = &content_type {
318 let value = HeaderValue::from_str(content_type)
319 .map_err(|error| ProviderError::InvalidRequest(error.to_string()))?;
320 wire_request.headers.insert(CONTENT_TYPE, value);
321 }
322 wire_request.body = encode_body(
323 request.body,
324 content_type.as_deref(),
325 &mut wire_request.headers,
326 )?;
327 let response = client
328 .fetch(wire_request)
329 .await
330 .map_err(|error| ProviderError::Network(error.to_string()))?;
331
332 if !response.status.is_success() {
333 #[cfg(feature = "tracing")]
334 tracing::warn!(
335 url = response.url.as_str(),
336 status = response.status.as_u16(),
337 "HTTP error status"
338 );
339 return Err(ProviderError::HttpStatus {
340 status: response.status,
341 url: response.url.to_string(),
342 });
343 }
344 Ok(response)
345 }
346 scheme => Err(ProviderError::UnsupportedScheme(scheme.to_owned())),
347 }
348 }
349}
350
351impl Provider {
359 async fn platform_fetch_inner(
360 client: Client,
361 request: FetchRequest,
362 ) -> Result<FetchResponse, FetchError> {
363 match request.url.scheme() {
364 "data" => Self::platform_fetch_data(request),
365 "file" => Self::platform_fetch_file(request),
366 "http" | "https" => client
367 .fetch(request)
368 .await
369 .map_err(|error| FetchError::Network(error.to_string())),
370 scheme => Err(FetchError::UnsupportedScheme(scheme.to_owned())),
371 }
372 }
373
374 fn platform_fetch_data(request: FetchRequest) -> Result<FetchResponse, FetchError> {
381 let data_url = DataUrl::process(request.url.as_str())
382 .map_err(|err| FetchError::InvalidRequest(format!("{err:?}")))?;
383 let mime = data_url.mime_type().to_string();
384 let (body, _) = data_url
385 .decode_to_vec()
386 .map_err(|err| FetchError::InvalidRequest(format!("{err:?}")))?;
387
388 let mut headers = HeaderMap::new();
389 if let Ok(value) = mime.parse() {
390 headers.insert(CONTENT_TYPE, value);
391 }
392
393 Ok(FetchResponse::new(request.url, StatusCode::OK)
394 .headers(headers)
395 .body(Bytes::from(body)))
396 }
397
398 fn platform_fetch_file(request: FetchRequest) -> Result<FetchResponse, FetchError> {
412 let path = request.url.to_file_path().map_err(|()| {
413 FetchError::InvalidRequest(format!("not a local path: {}", request.url))
414 })?;
415
416 let body = std::fs::read(path).map_err(|err| FetchError::Network(err.to_string()))?;
417
418 Ok(FetchResponse::new(request.url, StatusCode::OK).body(Bytes::from(body)))
419 }
420}
421
422impl FetchProvider for Provider {
423 fn fetch(&self, request: FetchRequest, handler: Box<dyn FetchHandler>) {
424 let client = self.client.clone();
425
426 #[cfg(feature = "tracing")]
427 let url = request.url.to_string();
428
429 spawn(async move {
430 let result = Self::platform_fetch_inner(client, request).await;
431
432 #[cfg(feature = "tracing")]
433 match &result {
434 Ok(response) => tracing::info!(
435 url = url.as_str(),
436 status = response.status.as_u16(),
437 "fetch complete"
438 ),
439 Err(error) => tracing::error!(url = url.as_str(), error = ?error, "fetch failed"),
440 }
441
442 handler.complete(result);
443 });
444 }
445}
446
447impl NetProvider for Provider {
448 fn fetch(&self, doc_id: usize, mut request: Request, handler: Box<dyn NetHandler>) {
449 let client = self.client.clone();
450
451 #[cfg(feature = "tracing")]
452 tracing::info!(url = request.url.as_str(), "Fetching");
453
454 let waker = self.waker.clone();
455 spawn(async move {
456 #[cfg(feature = "tracing")]
457 let url = request.url.to_string();
458
459 let signal = request.signal.take();
460 let result = if let Some(signal) = signal {
461 AbortFetch::new(
462 signal,
463 Box::pin(async move { Self::fetch_inner(client, request).await }),
464 )
465 .await
466 } else {
467 Self::fetch_inner(client, request).await
468 };
469
470 waker.wake(doc_id);
471
472 match result {
473 Ok((response_url, bytes)) => {
474 handler.bytes(response_url, bytes);
475 #[cfg(feature = "tracing")]
476 tracing::info!(url = url.as_str(), "Success fetching");
477 }
478 Err(e) => {
479 #[cfg(feature = "tracing")]
480 tracing::error!(url = url.as_str(), error = ?e, "Error fetching");
481 #[cfg(not(feature = "tracing"))]
482 let _ = e;
483 }
484 };
485 });
486 }
487}
488
489struct AbortFetch<F, T> {
490 signal: AbortSignal,
491 future: F,
492 _rt: PhantomData<T>,
493}
494
495impl<F, T> AbortFetch<F, T> {
496 fn new(signal: AbortSignal, future: F) -> Self {
497 Self {
498 signal,
499 future,
500 _rt: PhantomData,
501 }
502 }
503}
504
505impl<F, T> Future for AbortFetch<F, T>
506where
507 F: Future + Unpin + 'static,
508 F::Output: Into<Result<T, ProviderError>> + 'static,
509 T: Unpin,
510{
511 type Output = Result<T, ProviderError>;
512
513 fn poll(
514 mut self: std::pin::Pin<&mut Self>,
515 cx: &mut std::task::Context<'_>,
516 ) -> std::task::Poll<Self::Output> {
517 if self.signal.aborted() {
518 return Poll::Ready(Err(ProviderError::Abort));
519 }
520
521 match Pin::new(&mut self.future).poll(cx) {
522 Poll::Ready(output) => Poll::Ready(output.into()),
523 Poll::Pending => Poll::Pending,
524 }
525 }
526}
527
528#[derive(Debug)]
529pub enum ProviderError {
530 Abort,
531 Io(std::io::Error),
532 DataUrl(data_url::DataUrlError),
533 DataUrlBase64(data_url::forgiving_base64::InvalidBase64),
534 Network(String),
535 InvalidRequest(String),
536 UnsupportedScheme(String),
537 HttpStatus {
538 status: StatusCode,
539 url: String,
540 },
541}
542
543impl std::fmt::Display for ProviderError {
544 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545 match self {
546 Self::Abort => write!(f, "request aborted"),
547 Self::Io(e) => write!(f, "io error: {e}"),
548 Self::DataUrl(e) => write!(f, "data url error: {e:?}"),
549 Self::DataUrlBase64(e) => write!(f, "data url base64 error: {e:?}"),
550 Self::Network(e) => write!(f, "network error: {e}"),
551 Self::InvalidRequest(e) => write!(f, "invalid request: {e}"),
552 Self::UnsupportedScheme(scheme) => write!(f, "unsupported URL scheme: {scheme}"),
553 Self::HttpStatus { status, url } => write!(f, "HTTP {status} for {url}"),
554 }
555 }
556}
557
558impl std::error::Error for ProviderError {}
559
560impl From<std::io::Error> for ProviderError {
561 fn from(value: std::io::Error) -> Self {
562 Self::Io(value)
563 }
564}
565
566impl From<data_url::DataUrlError> for ProviderError {
567 fn from(value: data_url::DataUrlError) -> Self {
568 Self::DataUrl(value)
569 }
570}
571
572impl From<data_url::forgiving_base64::InvalidBase64> for ProviderError {
573 fn from(value: data_url::forgiving_base64::InvalidBase64) -> Self {
574 Self::DataUrlBase64(value)
575 }
576}
577
578fn encode_body(
579 body: Body,
580 content_type: Option<&str>,
581 headers: &mut HeaderMap,
582) -> Result<Option<Bytes>, ProviderError> {
583 match body {
584 Body::Bytes(bytes) => Ok(Some(bytes)),
585 Body::Empty => Ok(None),
586 Body::Form(form) => {
587 let mime = content_type
588 .and_then(|value| value.split(';').next())
589 .map(str::trim)
590 .unwrap_or("");
591 if mime.eq_ignore_ascii_case("application/x-www-form-urlencoded") {
592 let mut encoded = url::form_urlencoded::Serializer::new(String::new());
593 for entry in &form.0 {
594 encoded.append_pair(&entry.name, entry.value.as_ref());
595 }
596 return Ok(Some(Bytes::from(encoded.finish())));
597 }
598 if mime.eq_ignore_ascii_case("multipart/form-data") {
599 #[cfg(all(feature = "multipart", not(target_arch = "wasm32")))]
600 {
601 return encode_multipart(form, headers).map(Some);
602 }
603 #[cfg(not(all(feature = "multipart", not(target_arch = "wasm32"))))]
604 {
605 let _ = headers;
606 return Err(ProviderError::InvalidRequest(
607 "multipart requires the native multipart feature".to_owned(),
608 ));
609 }
610 }
611 Err(ProviderError::InvalidRequest(
612 "unsupported form Content-Type".to_owned(),
613 ))
614 }
615 }
616}
617
618#[cfg(all(feature = "multipart", not(target_arch = "wasm32")))]
619fn encode_multipart(
620 form: blitz_traits::net::FormData,
621 headers: &mut HeaderMap,
622) -> Result<Bytes, ProviderError> {
623 use blitz_traits::net::{Entry, EntryValue};
624 use psc_nanoid::{Nanoid, alphabet::Base64UrlAlphabet};
625 use std::io::Read;
626
627 let boundary = Nanoid::<21, Base64UrlAlphabet>::new().to_string();
628 let mut encoded = Vec::new();
629 for Entry { name, value } in form.0 {
630 let mut head = format!(
631 "--{boundary}\r\nContent-Disposition: form-data; name=\"{}\"",
632 multipart_parameter(&name),
633 );
634 let body = match value {
635 EntryValue::String(value) => Bytes::from(value),
636 EntryValue::File(path) => {
637 let name = path.file_name().unwrap_or_default().to_string_lossy();
638 head.push_str(&format!(
639 "; filename=\"{}\"\r\nContent-Type: application/octet-stream",
640 multipart_parameter(&name),
641 ));
642 let file = std::fs::File::open(path)?;
643 let mut body = Vec::new();
644 file.take(h1::MAX_BODY_BYTES as u64 + 1)
645 .read_to_end(&mut body)?;
646 if body.len() > h1::MAX_BODY_BYTES {
647 return Err(ProviderError::InvalidRequest(
648 "multipart file exceeds the body limit".to_owned(),
649 ));
650 }
651 Bytes::from(body)
652 }
653 EntryValue::EmptyFile => {
654 head.push_str("; filename=\"\"\r\nContent-Type: application/octet-stream");
655 Bytes::new()
656 }
657 };
658 head.push_str("\r\n\r\n");
659 let required = head
660 .len()
661 .checked_add(body.len())
662 .and_then(|length| length.checked_add(2))
663 .and_then(|length| length.checked_add(encoded.len()))
664 .ok_or_else(|| ProviderError::InvalidRequest("multipart body is too large".to_owned()))?;
665 if required > h1::MAX_BODY_BYTES {
666 return Err(ProviderError::InvalidRequest(
667 "multipart body exceeds the body limit".to_owned(),
668 ));
669 }
670 encoded.extend_from_slice(head.as_bytes());
671 encoded.extend_from_slice(&body);
672 encoded.extend_from_slice(b"\r\n");
673 }
674 let end = format!("--{boundary}--\r\n");
675 if end.len() > h1::MAX_BODY_BYTES.saturating_sub(encoded.len()) {
676 return Err(ProviderError::InvalidRequest(
677 "multipart body exceeds the body limit".to_owned(),
678 ));
679 }
680 encoded.extend_from_slice(end.as_bytes());
681 let content_type = HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
682 .map_err(|error| ProviderError::InvalidRequest(error.to_string()))?;
683 headers.insert(CONTENT_TYPE, content_type);
684 Ok(Bytes::from(encoded))
685}
686
687#[cfg(all(feature = "multipart", not(target_arch = "wasm32")))]
688fn multipart_parameter(value: &str) -> String {
689 value
690 .replace('\r', "%0D")
691 .replace('\n', "%0A")
692 .replace('"', "%22")
693}
694
695struct DummyNetWaker;
696impl NetWaker for DummyNetWaker {
697 fn wake(&self, _client_id: usize) {}
698}