seq-runtime 7.4.4

Runtime library for the Seq programming language
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
//! End-to-end tests for the HTTP client against a same-process server.
//!
//! These run the request through the full wire + pool stack, bypassing
//! only the SSRF validator (since the test server lives on 127.0.0.1
//! and the validator would correctly block that).

use super::pool;
use super::request::{perform_request, perform_request_with_target};
use super::ssrf::{Scheme, ValidatedTarget};
use crate::seqstring::global_string;
use crate::value::{MapKey, Value};
use may::net::TcpListener;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

/// Server response shape selector. Each integration test picks the
/// flavour that exercises the wire path it cares about.
#[derive(Clone, Copy)]
enum ServerMode {
    /// Read request headers only, ignore any body, reply `world` with
    /// `Content-Length: 5` and `Connection: keep-alive`.
    FixedBody,
    /// Read Content-Length bytes after the request blank line, echo
    /// them back in the response body. Used by the POST round-trip
    /// test to verify the body bytes actually reach the wire.
    Echo,
    /// Reply with `Transfer-Encoding: chunked`, two data chunks plus
    /// the terminator. Used by the chunked-decode integration test.
    Chunked,
    /// Reply with `Connection: close` + Content-Length, then close
    /// the socket from the server side. Used to verify the pool
    /// drops close-marked entries instead of trying to reuse them.
    CloseOnce,
}

/// Run a minimal HTTP/1.1 server strand on `listener` that loops
/// per-connection. Keep-alive is honoured for multi-request tests.
fn spawn_test_server(
    listener: TcpListener,
    accept_count: Arc<AtomicUsize>,
    req_count: Arc<AtomicUsize>,
    mode: ServerMode,
) {
    unsafe {
        may::coroutine::spawn(move || {
            for stream in listener.incoming() {
                let mut stream = match stream {
                    Ok(s) => s,
                    Err(_) => break,
                };
                accept_count.fetch_add(1, Ordering::SeqCst);
                let req_count = req_count.clone();
                may::coroutine::spawn(move || {
                    let stream_clone = match stream.try_clone() {
                        Ok(s) => s,
                        Err(_) => return,
                    };
                    let mut reader = BufReader::new(stream_clone);
                    loop {
                        // Read request lines until the blank-line terminator.
                        // Capture Content-Length so we can read the body
                        // for the echo mode.
                        let mut saw_request = false;
                        let mut content_length: usize = 0;
                        loop {
                            let mut line = String::new();
                            match reader.read_line(&mut line) {
                                Ok(0) => return,
                                Ok(_) => {}
                                Err(_) => return,
                            }
                            if line == "\r\n" || line == "\n" {
                                if saw_request {
                                    break;
                                }
                                continue;
                            }
                            saw_request = true;
                            if let Some(rest) = line.strip_prefix("Content-Length:") {
                                content_length = rest.trim().parse::<usize>().unwrap_or(0);
                            } else if let Some(rest) = line.strip_prefix("content-length:") {
                                content_length = rest.trim().parse::<usize>().unwrap_or(0);
                            }
                        }
                        req_count.fetch_add(1, Ordering::SeqCst);

                        let body_in: Vec<u8> =
                            if content_length > 0 && matches!(mode, ServerMode::Echo) {
                                let mut buf = vec![0u8; content_length];
                                if reader.read_exact(&mut buf).is_err() {
                                    return;
                                }
                                buf
                            } else {
                                Vec::new()
                            };

                        let write_ok = match mode {
                            ServerMode::FixedBody => {
                                let body = b"world";
                                let header = format!(
                                    "HTTP/1.1 200 OK\r\n\
                                     Content-Type: text/plain\r\n\
                                     Content-Length: {}\r\n\
                                     Connection: keep-alive\r\n\
                                     \r\n",
                                    body.len()
                                );
                                stream.write_all(header.as_bytes()).is_ok()
                                    && stream.write_all(body).is_ok()
                            }
                            ServerMode::Echo => {
                                let header = format!(
                                    "HTTP/1.1 200 OK\r\n\
                                     Content-Type: application/octet-stream\r\n\
                                     Content-Length: {}\r\n\
                                     Connection: keep-alive\r\n\
                                     \r\n",
                                    body_in.len()
                                );
                                stream.write_all(header.as_bytes()).is_ok()
                                    && stream.write_all(&body_in).is_ok()
                            }
                            ServerMode::Chunked => {
                                // Two chunks ("Hello" + " world") followed by
                                // the 0-terminator. Exercises chunk-size hex
                                // parsing, multi-chunk accumulation, and the
                                // trailer-section drain on the client side.
                                let payload = b"HTTP/1.1 200 OK\r\n\
                                     Content-Type: text/plain\r\n\
                                     Transfer-Encoding: chunked\r\n\
                                     Connection: keep-alive\r\n\
                                     \r\n\
                                     5\r\nHello\r\n\
                                     6\r\n world\r\n\
                                     0\r\n\r\n";
                                stream.write_all(payload).is_ok()
                            }
                            ServerMode::CloseOnce => {
                                // Reply with `Connection: close` + a fixed
                                // body, then return from this per-connection
                                // strand. Returning drops `stream`, which
                                // sends FIN to the peer — the client's
                                // pool::release call sees keep_alive=false
                                // and drops the entry. The next request to
                                // this listener must trigger a fresh accept.
                                let body = b"closed";
                                let header = format!(
                                    "HTTP/1.1 200 OK\r\n\
                                     Content-Type: text/plain\r\n\
                                     Content-Length: {}\r\n\
                                     Connection: close\r\n\
                                     \r\n",
                                    body.len()
                                );
                                let _ = stream.write_all(header.as_bytes());
                                let _ = stream.write_all(body);
                                return;
                            }
                        };
                        if !write_ok {
                            return;
                        }
                    }
                });
            }
        });
    }
}

fn loopback_target(port: u16, path: &str) -> ValidatedTarget {
    ValidatedTarget {
        scheme: Scheme::Http,
        host: "127.0.0.1".to_string(),
        port,
        addrs: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)],
        path_and_query: path.to_string(),
    }
}

fn unwrap_response(value: &Value) -> (i64, Vec<u8>, bool) {
    let map = match value {
        Value::Map(m) => m,
        other => panic!("expected response Map, got {:?}", other),
    };
    let status = match map.get(&MapKey::String(global_string("status".to_string()))) {
        Some(Value::Int(n)) => *n,
        other => panic!("status missing/wrong type: {:?}", other),
    };
    let body = match map.get(&MapKey::String(global_string("body".to_string()))) {
        Some(Value::String(s)) => s.as_bytes().to_vec(),
        other => panic!("body missing/wrong type: {:?}", other),
    };
    let ok = match map.get(&MapKey::String(global_string("ok".to_string()))) {
        Some(Value::Bool(b)) => *b,
        other => panic!("ok missing/wrong type: {:?}", other),
    };
    (status, body, ok)
}

#[test]
// Every test in this file that calls `perform_request*` reads the
// process-wide HTTP_REQUEST_TIMEOUT_OVERRIDE (so the deadline-pass
// timeout test can drive a short deadline). Tag them all into the
// same serial group so the override set by
// `http_request_timeout_fires_on_silent_server` can't leak into
// concurrent HTTP reads. The cost is intra-file parallelism; the
// benefit is no override-leak flakes under loaded CI.
#[serial_test::serial(http_global_state)]
fn http_get_end_to_end_against_local_server() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();

    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();

    let accept_count = Arc::new(AtomicUsize::new(0));
    let req_count = Arc::new(AtomicUsize::new(0));
    spawn_test_server(
        listener,
        accept_count.clone(),
        req_count.clone(),
        ServerMode::FixedBody,
    );

    let target = loopback_target(port, "/hello");
    let resp = perform_request_with_target("GET", target, None);
    let (status, body, ok) = unwrap_response(&resp);
    assert_eq!(status, 200);
    assert_eq!(body, b"world");
    assert!(ok);
    assert_eq!(req_count.load(Ordering::SeqCst), 1);
    assert_eq!(accept_count.load(Ordering::SeqCst), 1);
}

#[test]
#[serial_test::serial(http_global_state)]
fn http_pool_reuses_connection_for_second_request() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();

    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();

    let accept_count = Arc::new(AtomicUsize::new(0));
    let req_count = Arc::new(AtomicUsize::new(0));
    spawn_test_server(
        listener,
        accept_count.clone(),
        req_count.clone(),
        ServerMode::FixedBody,
    );

    let target1 = loopback_target(port, "/first");
    let r1 = perform_request_with_target("GET", target1, None);
    assert_eq!(unwrap_response(&r1).0, 200);

    let target2 = loopback_target(port, "/second");
    let r2 = perform_request_with_target("GET", target2, None);
    assert_eq!(unwrap_response(&r2).0, 200);

    // Two requests handled, but only ONE accept on the server side —
    // proves the pool returned the same connection for the second
    // request rather than redialing.
    assert_eq!(req_count.load(Ordering::SeqCst), 2);
    assert_eq!(
        accept_count.load(Ordering::SeqCst),
        1,
        "second request should reuse pooled connection (saw {} accepts)",
        accept_count.load(Ordering::SeqCst)
    );
}

#[test]
#[serial_test::serial(http_global_state)]
fn http_post_with_body_round_trips() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();

    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();
    let accept_count = Arc::new(AtomicUsize::new(0));
    let req_count = Arc::new(AtomicUsize::new(0));
    // Echo mode reads Content-Length bytes after the request blank
    // line and echoes them back. That makes this test actually prove
    // the body bytes traversed the wire — a regression in
    // `wire::write_request` that silently dropped the body would
    // surface here as a mismatch instead of a misleading 200 OK.
    spawn_test_server(
        listener,
        accept_count.clone(),
        req_count.clone(),
        ServerMode::Echo,
    );

    let target = loopback_target(port, "/create");
    let body = b"{\"name\":\"alice\"}";
    let resp =
        perform_request_with_target("POST", target, Some(("application/json", body.as_slice())));
    let (status, echoed, ok) = unwrap_response(&resp);
    assert_eq!(status, 200);
    assert!(ok);
    assert_eq!(echoed, body, "POST body must round-trip byte-for-byte");
}

#[test]
#[serial_test::serial(http_global_state)]
fn http_chunked_response_decodes_end_to_end() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();

    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();
    let accept_count = Arc::new(AtomicUsize::new(0));
    let req_count = Arc::new(AtomicUsize::new(0));
    spawn_test_server(
        listener,
        accept_count.clone(),
        req_count.clone(),
        ServerMode::Chunked,
    );

    let target = loopback_target(port, "/chunked");
    let resp = perform_request_with_target("GET", target, None);
    let (status, body, ok) = unwrap_response(&resp);
    assert_eq!(status, 200);
    assert!(ok);
    // Server emits two chunks: "Hello" (5) + " world" (6). The client
    // must concatenate them in order.
    assert_eq!(body, b"Hello world");
    assert_eq!(req_count.load(Ordering::SeqCst), 1);
}

#[test]
#[serial_test::serial(http_global_state)]
fn http_connection_close_response_evicts_from_pool() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();

    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();
    let accept_count = Arc::new(AtomicUsize::new(0));
    let req_count = Arc::new(AtomicUsize::new(0));
    spawn_test_server(
        listener,
        accept_count.clone(),
        req_count.clone(),
        ServerMode::CloseOnce,
    );

    let key = pool::PoolKey {
        scheme: Scheme::Http,
        host: "127.0.0.1".to_string(),
        port,
    };

    let target1 = loopback_target(port, "/first");
    let r1 = perform_request_with_target("GET", target1, None);
    assert_eq!(unwrap_response(&r1).0, 200);

    // The critical assertion: after r1's release, the
    // `Connection: close` entry must NOT be in the pool. This pins
    // the release-path behaviour directly. The accept-count check
    // below catches a similar shape but can be rescued by
    // `is_reusable`'s POLLIN/FIN detection on localhost (FIN
    // propagation is sub-microsecond), so a regression where
    // `release` ignores `keep_alive=false` would silently slip past
    // it. Asserting on pool state closes that gap.
    assert_eq!(
        pool::idle_count_for_test(&key),
        0,
        "Connection: close response must not survive pool::release. \
         An entry here means release ignored keep_alive=false."
    );

    let target2 = loopback_target(port, "/second");
    let r2 = perform_request_with_target("GET", target2, None);
    assert_eq!(unwrap_response(&r2).0, 200);

    // The second request must dial fresh — accept_count == 2 is the
    // observable consequence of the pool-drop above. (On localhost
    // this also catches compound regressions where both release and
    // is_reusable misbehave; the idle_count_for_test assertion above
    // is the primary anchor for the targeted property.)
    assert_eq!(req_count.load(Ordering::SeqCst), 2);
    assert_eq!(
        accept_count.load(Ordering::SeqCst),
        2,
        "Connection: close response must NOT be pooled: each request \
         should trigger a fresh accept on the server."
    );
}

// -----------------------------------------------------------------------------
// SSRF DNS-rebinding closure: the architectural property that perform_request
// calls dns::resolve at most once per request, and that the validated address
// list flows from SSRF straight into connect — not re-resolved downstream.
// A regression that passes `host: String` to connect (and re-resolves) would
// fail this loudly.
// -----------------------------------------------------------------------------

#[test]
#[serial_test::serial(dns_global_state)]
fn ssrf_dns_rebinding_closure_holds_at_most_one_resolve_per_request() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();
    crate::dns::clear_scripted_responses();
    crate::dns::reset_resolve_call_count();

    // Scripted DNS response. We push a single address that SSRF
    // *accepts* — 224.0.0.1 is an IPv4 multicast address (224.0.0.0/4
    // per RFC 5771) and isn't in any of the SSRF-blocked ranges, so
    // validate_url passes it through to connect. The TCP connect
    // then fails fast with `ENETUNREACH` (Linux kernel rejects
    // TCP-to-multicast in microseconds, not seconds) which keeps
    // this test cheap. The exact failure mode is irrelevant; this
    // test cares about call count, not connection outcome.
    //
    // Avoid TEST-NET-1 (192.0.2.0/24) here — that one takes the full
    // OS SYN timeout (~60s) on boxes that route it through a default
    // gateway, which would make the test prohibitively slow.
    crate::dns::push_scripted_response(vec!["224.0.0.1".to_string()]);

    // We don't care about the response body or status. Issue the
    // request and let it fail at the connect stage.
    let _resp = perform_request("GET", "http://example.com/", None);

    let count = crate::dns::resolve_call_count();
    assert_eq!(
        count, 1,
        "perform_request must call dns::resolve at most once per \
         request. count={count} > 1 means the connect or pool path \
         is re-resolving the hostname (DNS-rebinding regression): \
         the SSRF-validated address list should flow into connect, \
         not the original hostname string."
    );
}

// -----------------------------------------------------------------------------
// Happy-path TLS: end-to-end HTTPS request against a same-process rustls
// server. The cert is self-signed at test time (via rcgen) and serves as its
// own trust anchor; the runtime's TLS config is overridden for the duration
// of the test to trust it. Pins:
//
//   - net.tls.client handshake against a real rustls server completes.
//   - The TLS-wrapped Socket round-trips an HTTP/1.1 request and response.
//   - StreamOwned<ClientConnection, may::net::TcpStream>'s Read/Write impls
//     yield cooperatively through the may stream (test runs without
//     deadlocking the carrier).
// -----------------------------------------------------------------------------

fn build_tls_test_pair() -> (Arc<rustls::ServerConfig>, Arc<rustls::ClientConfig>) {
    // Self-signed cert with SAN=DNS:localhost. The CN matters because
    // the client SNI is "localhost" (see the target below); rustls
    // validates that one SAN entry covers the SNI.
    let rcgen::CertifiedKey { cert, key_pair } =
        rcgen::generate_simple_self_signed(vec!["localhost".to_string()])
            .expect("generate self-signed cert");

    let cert_der: rustls::pki_types::CertificateDer<'static> = cert.der().clone();
    let key_der: rustls::pki_types::PrivateKeyDer<'static> =
        rustls::pki_types::PrivatePkcs8KeyDer::from(key_pair.serialize_der()).into();

    let server_config = rustls::ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(vec![cert_der.clone()], key_der)
        .expect("server config");

    let mut roots = rustls::RootCertStore::empty();
    roots.add(cert_der).expect("add test cert as trust root");
    let client_config = rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_no_client_auth();

    (Arc::new(server_config), Arc::new(client_config))
}

/// Spawn a TLS-speaking server on its own OS thread.
///
/// Deliberately uses `std::thread` + `std::net::TcpListener` rather
/// than `may::coroutine::spawn` + `may::net::TcpListener`. The TLS
/// handshake is a multi-round-trip dance: each round requires the
/// server side to be scheduled, read the next message, and write a
/// response. When `cargo test` runs the entire runtime suite in
/// parallel, the may worker pool ends up saturated with coroutines
/// from other tests and the handshake stalls. Putting the server on
/// a dedicated OS thread sidesteps may's scheduler entirely — the
/// kernel keeps the thread runnable as long as there's a CPU.
fn spawn_tls_test_server(
    listener: std::net::TcpListener,
    server_config: Arc<rustls::ServerConfig>,
) {
    std::thread::spawn(move || {
        for stream in listener.incoming() {
            let stream = match stream {
                Ok(s) => s,
                Err(_) => return,
            };
            let server_config = server_config.clone();
            std::thread::spawn(move || {
                let conn = match rustls::ServerConnection::new(server_config) {
                    Ok(c) => c,
                    Err(_) => return,
                };
                let mut tls = rustls::StreamOwned::new(conn, stream);

                // Read request headers up to (and including) the
                // blank-line terminator. The TLS handshake happens
                // transparently on the first read; rustls drives
                // it via complete_io internally.
                let mut reader = BufReader::new(&mut tls);
                let mut saw_request = false;
                loop {
                    let mut line = String::new();
                    match reader.read_line(&mut line) {
                        Ok(0) | Err(_) => return,
                        _ => {}
                    }
                    if line == "\r\n" || line == "\n" {
                        if saw_request {
                            break;
                        }
                        continue;
                    }
                    saw_request = true;
                }
                drop(reader); // releases the &mut tls borrow

                let body = b"hello-over-tls";
                let header = format!(
                    "HTTP/1.1 200 OK\r\n\
                     Content-Type: text/plain\r\n\
                     Content-Length: {}\r\n\
                     Connection: close\r\n\
                     \r\n",
                    body.len()
                );
                let _ = tls.write_all(header.as_bytes());
                let _ = tls.write_all(body);
                let _ = tls.flush();
            });
        }
    });
}

/// RAII guard that clears the process-wide test TLS config on drop.
///
/// The HTTPS test installs a `ClientConfig` whose trust roots only
/// include the test's self-signed CA. If a panic between `install`
/// and the explicit `clear` leaks the override, other tests that
/// later use TLS would fail validation against real servers.
/// Dropping the guard unconditionally clears the override even on
/// the panic path.
struct TestTlsConfigGuard;
impl Drop for TestTlsConfigGuard {
    fn drop(&mut self) {
        crate::tls::clear_test_tls_config();
    }
}

#[test]
// Two serial groups stacked: `tls_global_state` against any future
// test that touches `TEST_TLS_CONFIG`, and `http_global_state` because
// this test reaches `run_once` and so reads `HTTP_REQUEST_TIMEOUT_OVERRIDE`.
// Without the second tag, the deadline-pass timeout test could race
// in (set override = 200ms) while this test is mid read_response and
// flip it to a wire error.
#[serial_test::serial(tls_global_state)]
#[serial_test::serial(http_global_state)]
fn https_round_trip_against_same_process_rustls_server() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();

    let (server_config, client_config) = build_tls_test_pair();
    crate::tls::install_test_tls_config(client_config);
    // The guard fires on every exit from this function, including
    // panic. Must be created AFTER install (so a build_tls_test_pair
    // failure isn't preceded by a no-op clear) and BEFORE any
    // panicking code below.
    let _tls_guard = TestTlsConfigGuard;

    // std::net::TcpListener here — the server runs on its own OS
    // thread to avoid may-scheduler congestion (see
    // spawn_tls_test_server's doc).
    let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();
    spawn_tls_test_server(listener, server_config);

    // Build the target by hand so we can drive HTTPS against a
    // loopback address (SSRF would otherwise block 127.0.0.1).
    // host=localhost matches the cert's SAN; addrs=127.0.0.1 is the
    // actual dial target. perform_validated takes the addrs verbatim
    // and the host string flows into the TLS handshake as SNI.
    let target = ValidatedTarget {
        scheme: Scheme::Https,
        host: "localhost".to_string(),
        port,
        addrs: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)],
        path_and_query: "/hello".to_string(),
    };
    let resp = perform_request_with_target("GET", target, None);
    let (status, body, ok) = unwrap_response(&resp);

    assert_eq!(status, 200, "HTTPS round-trip status");
    assert!(ok, "HTTPS round-trip ok flag");
    assert_eq!(body, b"hello-over-tls", "HTTPS round-trip body");

    // Defensive — the server emits Connection: close so nothing
    // should be pooled, but clear anyway so a future failure mode
    // doesn't leak a TLS-wrapped Socket into another test's pool.
    pool::clear_for_test();
    // _tls_guard drops here, restoring the production TLS config.
}

// -----------------------------------------------------------------------------
// Per-request HTTP timeout: a peer that accepts TCP but never sends a response
// must surface as a connection error within the configured deadline, not
// stall the carrier indefinitely. Pins the per-IO timeout plumbed through
// run_once. Uses the same std::thread/std::net pattern as the TLS test to
// avoid may-scheduler congestion under the full test suite.
// -----------------------------------------------------------------------------

/// RAII guard restoring the HTTP timeout override on drop. Mirrors
/// the TLS guard so a panic between override-install and the test's
/// happy-path cleanup can't leak short deadlines into other tests.
struct HttpRequestTimeoutGuard;
impl Drop for HttpRequestTimeoutGuard {
    fn drop(&mut self) {
        super::request::set_test_http_request_timeout(None);
    }
}

#[test]
#[serial_test::serial(http_global_state)]
fn http_request_timeout_fires_on_silent_server() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();

    // Drop deadline to 200ms so the test finishes quickly.
    super::request::set_test_http_request_timeout(Some(std::time::Duration::from_millis(200)));
    let _timeout_guard = HttpRequestTimeoutGuard;

    // Silent server: accept one TCP connection, then sit on it.
    // Never write a response. Uses std::net + std::thread so the
    // server doesn't fight the may scheduler.
    let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();
    std::thread::spawn(move || {
        if let Ok((stream, _)) = listener.accept() {
            // Hold the connection open until the test process exits.
            // The client side bails on its per-read timeout long
            // before this sleep ends.
            std::thread::sleep(std::time::Duration::from_secs(60));
            drop(stream);
        }
    });

    let target = loopback_target(port, "/silent");
    let start = std::time::Instant::now();
    let resp = perform_request_with_target("GET", target, None);
    let elapsed = start.elapsed();

    let (status, _body, ok) = unwrap_response(&resp);

    // The request must fail (status=0, ok=false) — the server never
    // wrote a response — and must do so within a small multiple of
    // the configured deadline. 2s is generous (we set 200ms); a
    // regression that ignored the timeout would block until the
    // server's 60s sleep ends.
    assert_eq!(status, 0, "silent-server request must surface as error");
    assert!(!ok, "ok flag must be false");
    assert!(
        elapsed < std::time::Duration::from_secs(2),
        "HTTP request must respect the per-IO timeout (elapsed={elapsed:?}). \
         Above 2s suggests the deadline isn't being plumbed through to \
         wire::read_response."
    );
}

// -----------------------------------------------------------------------------
// TLS handshake timeout: a peer that accepts plain TCP but never speaks TLS
// must surface as a handshake failure within the configured deadline. Pins
// the per-IO timeout that build_tls sets on the TcpStream before complete_io.
// -----------------------------------------------------------------------------

struct TlsHandshakeTimeoutGuard;
impl Drop for TlsHandshakeTimeoutGuard {
    fn drop(&mut self) {
        crate::tls::set_test_tls_handshake_timeout(None);
    }
}

#[test]
#[serial_test::serial(tls_global_state)]
fn tls_handshake_timeout_fires_on_silent_peer() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();

    // Need the test TLS config installed so build_tls doesn't fail
    // earlier (validation against webpki-roots), but the handshake
    // never actually progresses past the first read — the peer
    // doesn't speak TLS. The cert content doesn't matter here.
    let (_server_config, client_config) = build_tls_test_pair();
    crate::tls::install_test_tls_config(client_config);
    let _tls_guard = TestTlsConfigGuard;

    crate::tls::set_test_tls_handshake_timeout(Some(std::time::Duration::from_millis(200)));
    let _handshake_guard = TlsHandshakeTimeoutGuard;

    // Plain-TCP server: accept, never write. The client's
    // complete_io will read for the server's ServerHello, the
    // per-read timeout fires, build_tls returns Err(()).
    let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();
    std::thread::spawn(move || {
        if let Ok((stream, _)) = listener.accept() {
            std::thread::sleep(std::time::Duration::from_secs(60));
            drop(stream);
        }
    });

    let target = ValidatedTarget {
        scheme: Scheme::Https,
        host: "localhost".to_string(),
        port,
        addrs: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)],
        path_and_query: "/".to_string(),
    };
    let start = std::time::Instant::now();
    let resp = perform_request_with_target("GET", target, None);
    let elapsed = start.elapsed();

    let (status, _body, ok) = unwrap_response(&resp);

    assert_eq!(status, 0, "silent-peer TLS upgrade must surface as error");
    assert!(!ok);
    assert!(
        elapsed < std::time::Duration::from_secs(2),
        "TLS handshake must respect the per-IO timeout (elapsed={elapsed:?}). \
         Above 2s suggests build_tls isn't setting read/write timeouts on \
         the TcpStream before complete_io."
    );

    pool::clear_for_test();
}

// -----------------------------------------------------------------------------
// TCP connect timeout: dialing a routable but silent IP must surface as a
// connect failure within the configured deadline, not the kernel's default
// SYN retry budget (~60s on Linux). Pins the connect_timeout plumbed through
// `tcp::connect_to_addrs`.
// -----------------------------------------------------------------------------

/// RAII guard restoring the TCP connect timeout override on drop.
struct TcpConnectTimeoutGuard;
impl Drop for TcpConnectTimeoutGuard {
    fn drop(&mut self) {
        crate::tcp::set_test_tcp_connect_timeout(None);
    }
}

#[test]
#[serial_test::serial(http_global_state)]
fn tcp_connect_timeout_fires_on_silent_route() {
    unsafe { crate::scheduler::scheduler_init() };
    pool::clear_for_test();

    // Drop the connect deadline to 200ms.
    crate::tcp::set_test_tcp_connect_timeout(Some(std::time::Duration::from_millis(200)));
    let _connect_guard = TcpConnectTimeoutGuard;

    // 192.0.2.1 — TEST-NET-1 (RFC 5737). Reserved for documentation,
    // not announced. On a box with a default gateway, SYN to this IP
    // is routed and silently dropped at the upstream; the kernel
    // would normally retry until tcp_syn_retries exhausts (~60s on
    // Linux defaults). With our override applied, connect_timeout
    // returns ETIMEDOUT in ~200ms.
    //
    // SSRF doesn't block 192.0.2.x (see `is_dangerous_ipv4`); we use
    // `perform_request_with_target` to bypass SSRF anyway and dial
    // the address directly through `connect_to_addrs`.
    //
    // Caveat: a CI host with no default route to 192.0.2.x will get
    // ENETUNREACH in microseconds instead of the slow-SYN we're
    // measuring. The test still passes on such hosts (elapsed < 2s)
    // but doesn't actually exercise the timeout. The bound here
    // catches a regression where someone drops connect_timeout
    // entirely and falls back to plain `connect` — on a box with the
    // SYN-drop shape, that would balloon to 60s+.
    let target = ValidatedTarget {
        scheme: Scheme::Http,
        host: "192.0.2.1".to_string(),
        port: 80,
        addrs: vec![IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1))],
        path_and_query: "/".to_string(),
    };
    let start = std::time::Instant::now();
    let resp = perform_request_with_target("GET", target, None);
    let elapsed = start.elapsed();

    let (status, _body, ok) = unwrap_response(&resp);

    assert_eq!(status, 0, "silent-route connect must surface as error");
    assert!(!ok, "ok flag must be false");
    assert!(
        elapsed < std::time::Duration::from_secs(2),
        "TCP connect must respect the configured timeout (elapsed={elapsed:?}). \
         Above 2s suggests connect_to_addrs is calling plain connect, \
         not connect_timeout."
    );
}