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