1use super::{HostKey, HttpSender, NetworkConfig, NetworkError, NetworkRequest, SenderPool};
5
6use brotli_decompressor::Decompressor as BrotliDecompressor;
7use flate2::read::{DeflateDecoder as RawDeflateDecoder, GzDecoder, ZlibDecoder};
8use http_body_util::{BodyExt, Full};
9use hyper::{
10 Method, Request, Uri,
11 body::{Bytes, Incoming},
12 client::conn,
13 http::uri::Scheme,
14};
15use hyper_util::rt::{TokioExecutor, TokioIo};
16use rustls::{
17 ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme,
18 client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
19 crypto::CryptoProvider,
20 pki_types::{CertificateDer, ServerName, UnixTime},
21};
22use rustls_native_certs::load_native_certs;
23use serde::{Deserialize, Serialize};
24use std::io::Read as _;
25use std::sync::{Arc, RwLock};
26use tokio::{net::TcpStream, runtime::Runtime, task::LocalSet};
27use tokio_rustls::TlsConnector;
28
29const MAX_RESPONSE_BODY_BYTES: usize = 64 * 1024 * 1024;
33
34pub struct AsyncNetworkCore {
47 local: LocalSet,
48 rt: Runtime,
49 inner: Arc<SharedNetState>,
50 sender_pool: Arc<std::sync::RwLock<SenderPool>>,
51}
52
53impl AsyncNetworkCore {
54 pub fn new(inner: Arc<SharedNetState>) -> Self {
55 let rt = tokio::runtime::Builder::new_current_thread()
56 .enable_all()
57 .build()
58 .expect("failed to build tokio runtime");
59
60 let local = LocalSet::new();
61
62 Self {
63 rt,
64 local,
65 inner,
66 sender_pool: Arc::new(std::sync::RwLock::new(SenderPool::new())),
67 }
68 }
69
70 pub fn fetch_request_blocking(
72 &self,
73 request: &NetworkRequest,
74 ) -> Result<Response, NetworkError> {
75 self.local
76 .block_on(&self.rt, async { self.fetch_request(request).await })
77 }
78}
79
80#[derive(Debug, Deserialize, Serialize, Clone)]
81pub struct StatusCode(pub(crate) u16);
82
83impl From<hyper::StatusCode> for StatusCode {
84 fn from(value: hyper::StatusCode) -> Self {
85 Self(value.as_u16())
86 }
87}
88
89impl StatusCode {
90 pub fn as_u16(&self) -> u16 {
91 self.0
92 }
93
94 pub fn is_success(&self) -> bool {
95 (200..300).contains(&self.0)
96 }
97
98 pub fn is_redirection(&self) -> bool {
99 (300..400).contains(&self.0)
100 }
101
102 pub fn canonical_reason(&self) -> Option<&'static str> {
103 let hyper_code: hyper::StatusCode = self.as_u16().try_into().ok()?;
104 hyper_code.canonical_reason()
105 }
106}
107
108#[derive(Debug, Deserialize, Serialize, Clone)]
110pub struct Response {
111 pub url: String,
112 pub status: StatusCode,
113 pub reason_phrase: String,
114 pub headers: Vec<(String, String)>,
115 pub body: Vec<u8>,
116}
117
118pub struct SharedNetState {
126 tls_config: Arc<ClientConfig>,
127 insecure_tls_config: Arc<ClientConfig>,
128 network_config: RwLock<Arc<NetworkConfig>>,
129 cache: super::Cache,
130}
131
132impl SharedNetState {
133 pub fn new() -> Self {
134 Self {
135 tls_config: Arc::new(Self::build_tls_config(true)),
136 insecure_tls_config: Arc::new(Self::build_tls_config(false)),
137 network_config: RwLock::new(Arc::new(NetworkConfig::default())),
138 cache: super::Cache::new(),
139 }
140 }
141
142 pub fn set_network_config(&self, config: NetworkConfig) {
143 self.cache.set_enabled(config.enable_cache);
144 *self.network_config.write().unwrap() = Arc::new(config);
145 }
146
147 pub fn clear_cache(&self) {
149 self.cache.clear();
150 }
151
152 fn tls_config(&self) -> Arc<ClientConfig> {
154 if self.network_config.read().unwrap().verify_tls {
155 Arc::clone(&self.tls_config)
156 } else {
157 Arc::clone(&self.insecure_tls_config)
158 }
159 }
160
161 fn build_tls_config(verify_certs: bool) -> ClientConfig {
162 let mut roots = RootCertStore::empty();
163 let result = load_native_certs();
164
165 for cert in result.certs {
166 let _ = roots.add(cert);
167 }
168
169 let mut config = if verify_certs {
170 ClientConfig::builder()
171 .with_root_certificates(roots)
172 .with_no_client_auth()
173 } else {
174 let provider = CryptoProvider::get_default()
175 .expect("rustls default crypto provider must be installed");
176 ClientConfig::builder()
177 .dangerous()
178 .with_custom_certificate_verifier(Arc::new(SkipServerVerification(Arc::clone(
179 provider,
180 ))))
181 .with_no_client_auth()
182 };
183 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
184 config
185 }
186}
187
188impl Default for SharedNetState {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194impl AsyncNetworkCore {
195 pub async fn fetch_request(&self, request: &NetworkRequest) -> Result<Response, NetworkError> {
196 let mut current: Uri = request.url.parse().map_err(|_| NetworkError::InvalidUri)?;
197 let mut method = Method::from_bytes(request.method.as_bytes())
198 .map_err(|_| NetworkError::HttpRequestFailed)?;
199 let mut body = request.body.clone();
200 let mut headers = request.headers.clone();
201 let user_agent = self.inner.network_config.read().unwrap().user_agent.clone();
202 let mut redirects = 0usize;
203
204 loop {
205 let cache_headers = effective_cache_headers(&headers, &user_agent);
206 if method == Method::GET
207 && let Some(cached) = self
208 .inner
209 .cache
210 .get_for_request(¤t.to_string(), &cache_headers)
211 {
212 log::info!("NetworkCache: hit for url={}", current);
213 return Ok(cached);
214 }
215
216 let read_timeout = self.inner.network_config.read().unwrap().read_timeout;
217 let resp = tokio::time::timeout(
218 read_timeout,
219 self.send_request(¤t, &method, &headers, &body),
220 )
221 .await
222 .map_err(|_| NetworkError::Timeout)??;
223
224 if self.inner.network_config.read().unwrap().follow_redirects
225 && hyper::StatusCode::try_from(resp.status.0)
226 .map_err(|_| NetworkError::InvalidIpcStatusCode)?
227 .is_redirection()
228 {
229 if redirects >= 10 {
230 return Err(NetworkError::TooManyRedirects);
231 }
232
233 if let Some(loc) = resp
234 .headers
235 .iter()
236 .find(|(k, _)| k.eq_ignore_ascii_case("location"))
237 .map(|(_, v)| v)
238 {
239 let next = resolve_redirect(¤t, loc)?;
240 strip_origin_bound_headers_on_cross_origin_redirect(
241 ¤t,
242 &next,
243 &mut headers,
244 );
245 current = next;
246 if resp.status.as_u16() == 303
247 || ((resp.status.as_u16() == 301 || resp.status.as_u16() == 302)
248 && method == Method::POST)
249 {
250 method = Method::GET;
251 body.clear();
252 }
253 redirects += 1;
254 continue;
255 }
256 }
257
258 if method == Method::GET && resp.status.is_success() {
259 self.inner
260 .cache
261 .set_for_request(¤t.to_string(), &resp, &cache_headers);
262 }
263
264 return Ok(resp);
265 }
266 }
267
268 async fn send_request(
269 &self,
270 uri: &Uri,
271 method: &Method,
272 headers: &[(String, String)],
273 body: &[u8],
274 ) -> Result<Response, NetworkError> {
275 let host = uri.host().ok_or(NetworkError::MissingHost)?;
276 let scheme = uri.scheme().unwrap_or(&Scheme::HTTP);
277 let port = uri
278 .port_u16()
279 .unwrap_or(if scheme == &Scheme::HTTPS { 443 } else { 80 });
280
281 let key = HostKey {
282 scheme: scheme.clone(),
283 host: host.to_string(),
284 port,
285 };
286
287 let mut sender = self.get_or_create_sender(&key).await?;
288
289 let is_h2 = matches!(sender, HttpSender::Http2(_));
290 log::debug!(
291 target: "network",
292 "send via {} to {}",
293 if is_h2 { "h2" } else { "http1" },
294 uri
295 );
296 let request_uri = if is_h2 {
300 let authority = uri
301 .authority()
302 .map(|a| a.as_str().to_string())
303 .unwrap_or_else(|| format!("{}:{}", host, port));
304 format!(
305 "{}://{}{}",
306 scheme,
307 authority,
308 uri.path_and_query().map_or("/", |p| p.as_str())
309 )
310 } else {
311 uri.path_and_query().map_or("/", |p| p.as_str()).to_string()
312 };
313 let mut request = Request::builder()
314 .method(method.clone())
315 .uri(&request_uri)
316 .header(
317 "User-Agent",
318 self.inner.network_config.read().unwrap().user_agent.clone(),
319 );
320 if !is_h2 {
321 request = request.header("Host", host);
322 }
323 if !headers
324 .iter()
325 .any(|(name, _)| name.eq_ignore_ascii_case("accept-language"))
326 {
327 request = request.header(
328 "Accept-Language",
329 crate::platform::locale::accept_language_header(),
330 );
331 }
332 if !headers
333 .iter()
334 .any(|(name, _)| name.eq_ignore_ascii_case("accept-encoding"))
335 {
336 request = request.header("Accept-Encoding", "gzip, deflate, br");
337 }
338 for (name, value) in headers {
339 request = request.header(name, value);
340 }
341 let req = request
342 .body(Full::new(Bytes::copy_from_slice(body)))
343 .map_err(|_| NetworkError::HttpRequestFailed)?;
344
345 let mut res = match &mut sender {
346 HttpSender::Http1(s) => s
347 .send_request(req)
348 .await
349 .map_err(|_| NetworkError::HttpRequestFailed)?,
350 HttpSender::Http2(s) => s
351 .send_request(req)
352 .await
353 .map_err(|_| NetworkError::HttpRequestFailed)?,
354 };
355
356 let response = Self::collect_response(uri.to_string(), &mut res).await?;
357
358 self.sender_pool
359 .write()
360 .unwrap()
361 .add_connection(key, sender);
362
363 Ok(response)
364 }
365
366 async fn collect_response(
367 url: String,
368 res: &mut hyper::Response<Incoming>,
369 ) -> Result<Response, NetworkError> {
370 let status = res.status();
371 let reason_phrase = status.canonical_reason().unwrap_or("").to_string();
372
373 let headers = res
374 .headers()
375 .iter()
376 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
377 .collect();
378
379 let mut body = Vec::new();
380 while let Some(frame) = res.frame().await {
381 let frame = frame.map_err(|_| NetworkError::HttpResponseFailed)?;
382 if let Some(chunk) = frame.data_ref() {
383 if body.len().saturating_add(chunk.len()) > MAX_RESPONSE_BODY_BYTES {
384 return Err(NetworkError::ResponseTooLarge);
385 }
386 body.extend_from_slice(chunk);
387 }
388 }
389
390 let content_encoding = res
391 .headers()
392 .get("content-encoding")
393 .and_then(|v| v.to_str().ok())
394 .map(|v| v.to_string());
395 let body = Self::decode_content(content_encoding.as_deref(), body)?;
396
397 Ok(Response {
398 url,
399 status: status.into(),
400 reason_phrase,
401 headers,
402 body,
403 })
404 }
405
406 fn decode_content(encoding: Option<&str>, mut body: Vec<u8>) -> Result<Vec<u8>, NetworkError> {
411 let Some(encoding) = encoding else {
412 return Ok(body);
413 };
414 for part in encoding.split(',').map(|s| s.trim().to_ascii_lowercase()) {
415 let decoded = match part.as_str() {
416 "gzip" | "x-gzip" => Self::read_decompressed(GzDecoder::new(&body[..]))?,
417 "deflate" => match Self::read_decompressed(ZlibDecoder::new(&body[..]))? {
418 Some(decoded) => Some(decoded),
419 None => Self::read_decompressed(RawDeflateDecoder::new(&body[..]))?,
420 },
421 "br" => Self::read_decompressed(BrotliDecompressor::new(&body[..], 1 << 16))?,
422 _ => None,
423 };
424 if let Some(decoded) = decoded {
425 body = decoded;
426 }
427 }
428 Ok(body)
429 }
430
431 fn read_decompressed<R: std::io::Read>(reader: R) -> Result<Option<Vec<u8>>, NetworkError> {
438 let mut output = Vec::new();
439 let mut limited = reader.take(MAX_RESPONSE_BODY_BYTES as u64 + 1);
440 match limited.read_to_end(&mut output) {
441 Ok(_) if output.len() > MAX_RESPONSE_BODY_BYTES => Err(NetworkError::ResponseTooLarge),
442 Ok(_) => Ok(Some(output)),
443 Err(_) => Ok(None),
444 }
445 }
446
447 async fn get_or_create_sender(&self, key: &HostKey) -> Result<HttpSender, NetworkError> {
448 if let Some(s) = self.sender_pool.write().unwrap().get_connection(key) {
449 return Ok(s);
450 }
451
452 self.create_connection(key).await
453 }
454
455 async fn create_connection(&self, key: &HostKey) -> Result<HttpSender, NetworkError> {
456 let addr = format!("{}:{}", key.host, key.port);
457 let connect_timeout = self.inner.network_config.read().unwrap().connect_timeout;
458 let stream = tokio::time::timeout(connect_timeout, TcpStream::connect(addr))
459 .await
460 .map_err(|_| NetworkError::Timeout)?
461 .map_err(|_| NetworkError::ConnectionFailed)?;
462
463 if key.scheme == Scheme::HTTPS {
464 let tls = TlsConnector::from(self.inner.tls_config());
465 let key = key.clone();
466 let domain =
467 ServerName::try_from(key.host.clone()).map_err(|_| NetworkError::InvalidDnsName)?;
468
469 let stream = tls
470 .connect(domain, stream)
471 .await
472 .map_err(|_| NetworkError::TlsFailed)?;
473 let negotiated_h2 = stream.get_ref().1.alpn_protocol() == Some(b"h2");
474
475 let io = TokioIo::new(stream);
476 if negotiated_h2 {
477 let (sender, conn) = conn::http2::handshake(TokioExecutor::new(), io)
478 .await
479 .map_err(|_| NetworkError::HttpHandshakeFailed)?;
480 self.spawn_connection_task(conn, key);
481 Ok(HttpSender::Http2(sender))
482 } else {
483 let (sender, conn) = conn::http1::handshake(io)
484 .await
485 .map_err(|_| NetworkError::HttpHandshakeFailed)?;
486 self.spawn_connection_task(conn, key);
487 Ok(HttpSender::Http1(sender))
488 }
489 } else {
490 let (sender, conn) = conn::http1::handshake(TokioIo::new(stream))
491 .await
492 .map_err(|_| NetworkError::HttpHandshakeFailed)?;
493
494 self.spawn_connection_task(conn, key.clone());
495 Ok(HttpSender::Http1(sender))
496 }
497 }
498
499 fn spawn_connection_task<F>(&self, conn: F, key: HostKey)
500 where
501 F: std::future::Future<Output = Result<(), hyper::Error>> + 'static,
502 {
503 let pool = Arc::clone(&self.sender_pool);
504 tokio::task::spawn_local(async move {
505 let _ = conn.await;
506 pool.write().unwrap().remove_connection(&key);
507 });
508 }
509}
510
511#[derive(Debug)]
514struct SkipServerVerification(Arc<CryptoProvider>);
515
516impl ServerCertVerifier for SkipServerVerification {
517 fn verify_server_cert(
518 &self,
519 _end_entity: &CertificateDer<'_>,
520 _intermediates: &[CertificateDer<'_>],
521 _server_name: &ServerName<'_>,
522 _ocsp_response: &[u8],
523 _now: UnixTime,
524 ) -> Result<ServerCertVerified, rustls::Error> {
525 Ok(ServerCertVerified::assertion())
526 }
527
528 fn verify_tls12_signature(
529 &self,
530 _message: &[u8],
531 _cert: &CertificateDer<'_>,
532 _dss: &DigitallySignedStruct,
533 ) -> Result<HandshakeSignatureValid, rustls::Error> {
534 Ok(HandshakeSignatureValid::assertion())
535 }
536
537 fn verify_tls13_signature(
538 &self,
539 _message: &[u8],
540 _cert: &CertificateDer<'_>,
541 _dss: &DigitallySignedStruct,
542 ) -> Result<HandshakeSignatureValid, rustls::Error> {
543 Ok(HandshakeSignatureValid::assertion())
544 }
545
546 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
547 self.0.signature_verification_algorithms.supported_schemes()
548 }
549}
550
551fn resolve_redirect(base: &Uri, location: &str) -> Result<Uri, NetworkError> {
552 if location.starts_with("http://") || location.starts_with("https://") {
553 return location.parse().map_err(|_| NetworkError::InvalidUri);
554 }
555
556 let scheme = base.scheme_str().unwrap_or("https");
557 let authority = base.authority().ok_or(NetworkError::InvalidUri)?;
558
559 let next = if location.starts_with("//") {
560 format!("{scheme}:{location}")
561 } else if location.starts_with('/') {
562 format!("{scheme}://{}{location}", authority)
563 } else {
564 let base_path = base.path();
565 let prefix = base_path.rsplit_once('/').map_or("", |x| x.0);
566 format!("{scheme}://{}{prefix}/{location}", authority)
567 };
568
569 next.parse().map_err(|_| NetworkError::InvalidUri)
570}
571
572fn uri_origin(uri: &Uri) -> Option<String> {
573 let url = url::Url::parse(&uri.to_string()).ok()?;
574 Some(url.origin().ascii_serialization())
575}
576
577fn is_origin_bound_header(name: &str) -> bool {
578 matches!(
579 name.to_ascii_lowercase().as_str(),
580 "authorization" | "proxy-authorization" | "cookie" | "cookie2" | "host"
581 )
582}
583
584fn strip_origin_bound_headers(headers: &mut Vec<(String, String)>) {
585 headers.retain(|(name, _)| !is_origin_bound_header(name));
586}
587
588fn strip_origin_bound_headers_on_cross_origin_redirect(
589 current: &Uri,
590 next: &Uri,
591 headers: &mut Vec<(String, String)>,
592) {
593 let origins_match = matches!(
594 (uri_origin(current), uri_origin(next)),
595 (Some(current), Some(next)) if current == next
596 );
597 if !origins_match {
598 strip_origin_bound_headers(headers);
599 }
600}
601
602fn effective_cache_headers(
603 request_headers: &[(String, String)],
604 user_agent: &str,
605) -> Vec<(String, String)> {
606 let mut headers = request_headers.to_vec();
607 let defaults = [
608 ("user-agent", user_agent.to_string()),
609 (
610 "accept-language",
611 crate::platform::locale::accept_language_header(),
612 ),
613 ("accept-encoding", "gzip, deflate, br".to_string()),
614 ];
615 for (name, value) in defaults {
616 if !headers
617 .iter()
618 .any(|(header, _)| header.eq_ignore_ascii_case(name))
619 {
620 headers.push((name.to_string(), value));
621 }
622 }
623 headers
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use std::io::{Read, Write};
630 use std::net::TcpListener;
631 use std::thread;
632 use std::time::Duration;
633
634 fn spawn_keep_alive_server(listener: TcpListener) -> String {
639 let address = listener.local_addr().unwrap().to_string();
640 thread::spawn(move || {
641 while let Ok((stream, _)) = listener.accept() {
642 thread::spawn(move || serve_keep_alive_connection(stream));
643 }
644 });
645 address
646 }
647
648 #[test]
649 fn redirect_strips_credentials_only_when_origin_changes() {
650 let current: Uri = "https://example.test/start".parse().unwrap();
651 let same_origin: Uri = "https://example.test:443/next".parse().unwrap();
652 let cross_origin: Uri = "https://other.test/next".parse().unwrap();
653 let mut headers = vec![
654 ("Authorization".to_string(), "Bearer secret".to_string()),
655 ("Cookie".to_string(), "session=secret".to_string()),
656 ("X-Request".to_string(), "keep".to_string()),
657 ];
658
659 strip_origin_bound_headers_on_cross_origin_redirect(¤t, &same_origin, &mut headers);
660 assert_eq!(headers.len(), 3);
661 strip_origin_bound_headers_on_cross_origin_redirect(¤t, &cross_origin, &mut headers);
662 assert_eq!(headers, [("X-Request".to_string(), "keep".to_string())]);
663 }
664
665 #[test]
666 fn redirect_never_escalates_scheme_to_file() {
667 let base: Uri = "https://example.test/start".parse().unwrap();
668 let next = resolve_redirect(&base, "file:///etc/passwd").unwrap();
669 assert_eq!(next.scheme_str(), Some("https"));
670 assert_eq!(next.authority().unwrap().as_str(), "example.test");
671 }
672
673 #[test]
674 fn redirect_never_escalates_scheme_to_resource() {
675 let base: Uri = "https://example.test/start".parse().unwrap();
676 let next = resolve_redirect(&base, "resource:///devtools/index.html").unwrap();
677 assert_eq!(next.scheme_str(), Some("https"));
678 assert_eq!(next.authority().unwrap().as_str(), "example.test");
679 }
680
681 #[test]
682 fn redirect_keeps_http_and_https_absolute_locations() {
683 let base: Uri = "https://example.test/start".parse().unwrap();
684 assert_eq!(
685 resolve_redirect(&base, "http://other.test/x")
686 .unwrap()
687 .scheme_str(),
688 Some("http")
689 );
690 assert_eq!(
691 resolve_redirect(&base, "https://other.test/x")
692 .unwrap()
693 .scheme_str(),
694 Some("https")
695 );
696 }
697
698 fn serve_keep_alive_connection(mut stream: std::net::TcpStream) {
699 stream
700 .set_read_timeout(Some(Duration::from_secs(10)))
701 .unwrap();
702 let mut buffer = [0_u8; 4096];
703 loop {
704 let mut request = Vec::new();
705 loop {
706 let read = match stream.read(&mut buffer) {
707 Ok(0) | Err(_) => return,
708 Ok(read) => read,
709 };
710 request.extend_from_slice(&buffer[..read]);
711 if request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
712 break;
713 }
714 }
715 let request_line = String::from_utf8_lossy(&request)
716 .lines()
717 .next()
718 .unwrap_or("")
719 .to_string();
720 let body = format!("ok:{request_line}");
721 let response = format!(
722 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{body}",
723 body.len()
724 );
725 if stream.write_all(response.as_bytes()).is_err() {
726 return;
727 }
728 }
729 }
730
731 #[test]
732 fn enable_cache_config_is_applied_to_cache() {
733 let state = SharedNetState::new();
734 assert!(state.cache.is_enabled());
735
736 state.set_network_config(NetworkConfig {
737 enable_cache: false,
738 ..NetworkConfig::default()
739 });
740 assert!(!state.cache.is_enabled());
741 }
742
743 #[test]
744 fn sends_request_method_headers_and_body() {
745 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
746 let address = listener.local_addr().unwrap();
747 let server = thread::spawn(move || {
748 let (mut stream, _) = listener.accept().unwrap();
749 stream
750 .set_read_timeout(Some(Duration::from_secs(5)))
751 .unwrap();
752 let mut request = Vec::new();
753 let mut buffer = [0_u8; 1024];
754 loop {
755 let read = stream.read(&mut buffer).unwrap();
756 if read == 0 {
757 break;
758 }
759 request.extend_from_slice(&buffer[..read]);
760 let Some(header_end) = request.windows(4).position(|bytes| bytes == b"\r\n\r\n")
761 else {
762 continue;
763 };
764 let headers = String::from_utf8_lossy(&request[..header_end]);
765 let content_length = headers
766 .lines()
767 .find_map(|line| {
768 let (name, value) = line.split_once(':')?;
769 name.eq_ignore_ascii_case("content-length")
770 .then(|| value.trim().parse::<usize>().unwrap())
771 })
772 .unwrap_or(0);
773 if request.len() >= header_end + 4 + content_length {
774 break;
775 }
776 }
777 stream
778 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
779 .unwrap();
780 request
781 });
782
783 let core = AsyncNetworkCore::new(Arc::new(SharedNetState::new()));
784 let response = core
785 .fetch_request_blocking(&NetworkRequest {
786 url: format!("http://{address}/submit"),
787 method: "POST".to_string(),
788 headers: vec![("X-Orinium-Test".to_string(), "yes".to_string())],
789 body: b"hello".to_vec(),
790 })
791 .unwrap();
792 assert_eq!(response.body, b"ok");
793
794 let request = String::from_utf8(server.join().unwrap()).unwrap();
795 assert!(request.starts_with("POST /submit HTTP/1.1\r\n"));
796 assert!(request.to_ascii_lowercase().contains("x-orinium-test: yes"));
797 assert!(request.to_ascii_lowercase().contains("accept-language: "));
798 assert!(request.ends_with("\r\n\r\nhello"));
799 }
800
801 #[test]
802 fn pooled_connections_are_not_shared_between_worker_runtimes() {
803 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
804 let address = spawn_keep_alive_server(listener);
805
806 let shared = Arc::new(SharedNetState::new());
807 let core_a = AsyncNetworkCore::new(Arc::clone(&shared));
808 let core_b = AsyncNetworkCore::new(shared);
809
810 let first = core_a
813 .fetch_request_blocking(&NetworkRequest::get(format!("http://{address}/first")))
814 .unwrap();
815 assert_eq!(first.body, b"ok:GET /first HTTP/1.1");
816
817 assert!(
821 core_b.sender_pool.read().unwrap().is_empty(),
822 "A's pooled connection must not be visible to another worker"
823 );
824 let second = core_b
825 .fetch_request_blocking(&NetworkRequest::get(format!("http://{address}/second")))
826 .unwrap();
827 assert_eq!(second.body, b"ok:GET /second HTTP/1.1");
828
829 let third = core_a
831 .fetch_request_blocking(&NetworkRequest::get(format!("http://{address}/third")))
832 .unwrap();
833 assert_eq!(third.body, b"ok:GET /third HTTP/1.1");
834 }
835
836 #[test]
837 fn decode_content_gzip_round_trips() {
838 use flate2::{Compression, write::GzEncoder};
839 use std::io::Write as _;
840
841 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
842 encoder.write_all(b"hello world").unwrap();
843 let compressed = encoder.finish().unwrap();
844
845 let decoded =
846 AsyncNetworkCore::decode_content(Some("gzip"), compressed).expect("decodes gzip");
847 assert_eq!(decoded, b"hello world");
848 }
849
850 #[test]
851 fn decode_content_rejects_decompression_bomb() {
852 use flate2::{Compression, write::GzEncoder};
853 use std::io::Write as _;
854
855 let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
857 encoder
858 .write_all(&vec![0_u8; MAX_RESPONSE_BODY_BYTES + 1024])
859 .unwrap();
860 let compressed = encoder.finish().unwrap();
861 assert!(
862 compressed.len() < MAX_RESPONSE_BODY_BYTES,
863 "payload must be a bomb (small on the wire)"
864 );
865
866 assert!(matches!(
867 AsyncNetworkCore::decode_content(Some("gzip"), compressed),
868 Err(NetworkError::ResponseTooLarge)
869 ));
870 }
871}