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