Skip to main content

orinium_browser/platform/network/
core.rs

1//! ネットワークコア
2//! HTTP通信とレスポンス処理を担当する。
3
4use 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
29/// Upper bound on a decoded response body (before or after decompression),
30/// guarding against memory exhaustion from oversized or decompression-bomb
31/// responses.
32const MAX_RESPONSE_BODY_BYTES: usize = 64 * 1024 * 1024;
33
34/// Per-thread driver for the shared network state.
35///
36/// The tokio runtime and its [`LocalSet`] are `!Send`, so each pool worker
37/// owns one of these; the expensive state they operate on lives in the
38/// [`SharedNetState`] behind an `Arc` and is reused across workers.
39///
40/// The [`SenderPool`] deliberately lives *here*, not in the shared state: a
41/// pooled connection's driver task is spawned onto the creating worker's
42/// local set and is only polled while that worker runs a fetch. Sharing
43/// senders across runtimes would let one worker check out a connection whose
44/// driver is parked on another (idle) worker, leaving the request awaiting
45/// frames nobody ever reads.
46pub 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    /// Runs a fetch to completion on this worker's local set.
71    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/// HTTP response
109#[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
118/// TLS, cache and config shared by every fetch worker.
119///
120/// All fields are thread-safe: the cache is internally synchronized, and the
121/// config is swapped atomically through an `Arc` so workers observe updates
122/// without blocking a fetch in progress. Connection pools are *not* shared:
123/// each [`AsyncNetworkCore`] owns its pool because a connection is only valid
124/// on the runtime that drives it.
125pub 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    /// Removes all cached responses.
148    pub fn clear_cache(&self) {
149        self.cache.clear();
150    }
151
152    /// Returns the TLS configuration for the current `verify_tls` setting.
153    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(&current.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(&current, &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(&current, loc)?;
240                    strip_origin_bound_headers_on_cross_origin_redirect(
241                        &current,
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(&current.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        // HTTP/2 derives `:authority` from the request URI (a relative URI is
297        // rejected), while HTTP/1.1 wants an origin-form target plus a `Host`
298        // header, which hyper refuses to add for us.
299        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    /// Decodes a response body according to the `Content-Encoding` header value
407    /// (e.g. `gzip`, `deflate`, `br`). Unknown or undecodable encodings leave
408    /// the body untouched; a decoded body larger than
409    /// [`MAX_RESPONSE_BODY_BYTES`] is rejected.
410    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    /// Reads a decompression stream to completion.
432    ///
433    /// Returns `Ok(None)` when the stream cannot be decoded (matching the
434    /// previous lenient behavior of leaving the body untouched), and
435    /// `Err(ResponseTooLarge)` when the decoded output would exceed
436    /// [`MAX_RESPONSE_BODY_BYTES`].
437    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/// A `ServerCertVerifier` that accepts any server certificate (dev/diagnostic
512/// mode, used when `verify_tls` is disabled).
513#[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    /// Keep-alive HTTP/1.1 server answering every request with
635    /// `ok:<request line>`, so pooled connections stay open across sequential
636    /// fetches. Each connection is served until the peer hangs up. Returns
637    /// the bound address.
638    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(&current, &same_origin, &mut headers);
660        assert_eq!(headers.len(), 3);
661        strip_origin_bound_headers_on_cross_origin_redirect(&current, &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        // Worker A fetches and returns its keep-alive connection to its own
811        // pool; the connection stays open on A's local set.
812        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        // Worker B must open its own connection for the same host instead of
818        // checking out the sender parked on A's idle runtime, where nobody
819        // would ever poll it.
820        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        // A still reuses its own pooled connection.
830        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        // Compresses to a few KiB but expands past the body limit.
856        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}