Skip to main content

nodejs/stdlib/
https.rs

1//! Node `https` module: HTTP/1.1 over real TLS.
2//!
3//! Server: `https.createServer(options, requestListener)` builds a `tls` server
4//! (real rustls handshake, see `tls.rs`) whose per-connection hook attaches an
5//! HTTP/1.1 request parser. As decrypted bytes arrive (`tls::on_socket_data` →
6//! `https::feed`) we parse complete requests, build an `IncomingMessage` (`req`)
7//! and a response object (`res`), and call the user's `(req, res)` listener.
8//! `res.end` serializes an HTTP/1.1 response and writes it back through the TLS
9//! channel via `tls::socket_write`.
10//!
11//! Client: `https.request(options[, cb])` / `https.get(url[, cb])` open a blocking
12//! TLS connection on a background thread, write the request, read the full
13//! response (the request forces `Connection: close`, so read-to-EOF is reliable),
14//! parse it into an `IncomingMessage`, and fire `cb(res)` on the main thread.
15//!
16//! The HTTP/1.1 request parser and response serializer here are minimal
17//! reimplementations of the private logic in `http.rs` (`parse_request`,
18//! `serialize_response`). They are duplicated because that logic writes to `net`
19//! sockets via `net::socket_write_id`, whereas an https response must go through
20//! the TLS write channel. If `http::parse_request`/`ParsedReq`/`serialize_response`/
21//! `ResState` were made `pub` (and response writing were sink-agnostic), https
22//! could delegate to them instead.
23
24use crate::host::{invoke, with_host, JsObj};
25use fusevm::Value;
26use indexmap::IndexMap;
27use rustls::pki_types::ServerName;
28use rustls::{ClientConnection, StreamOwned};
29use std::collections::HashMap;
30use std::io::{Read, Write};
31use std::net::TcpStream;
32
33/// `https` module functions routed through `stdlib::call`.
34pub const MODULE_METHODS: &[&str] = &["createServer", "request", "get"];
35
36/// Instance method names for this module's `@@native` tags (property reads that
37/// yield a bound method), exposed to `stdlib::instance_has_method`.
38pub const RESPONSE_METHODS: &[&str] = &[
39    "writeHead",
40    "setHeader",
41    "getHeader",
42    "getHeaderNames",
43    "getHeaders",
44    "hasHeader",
45    "removeHeader",
46    "write",
47    "end",
48    "flushHeaders",
49];
50pub const CLIENT_REQUEST_METHODS: &[&str] = &[
51    "write",
52    "end",
53    "setHeader",
54    "getHeader",
55    "removeHeader",
56    "abort",
57    "destroy",
58    "setTimeout",
59];
60
61// ── module-level non-function values (https.Agent / globalAgent) ─────────────
62
63/// `https.Agent` / `https.globalAgent`: a minimal stub. node-js opens a fresh
64/// connection per request (no pooling / keep-alive reuse), so an Agent carries no
65/// behavior beyond being a constructible/inspectable object.
66pub fn constant(name: &str) -> Option<Value> {
67    match name {
68        "Agent" => Some(with_host(|h| h.alloc(JsObj::Builtin("https.Agent".into())))),
69        "globalAgent" => Some(with_host(|h| {
70            let mut m = IndexMap::new();
71            m.insert("@@native".into(), h.new_str("Agent"));
72            m.insert("maxSockets".into(), Value::Float(f64::INFINITY));
73            m.insert("protocol".into(), h.new_str("https:"));
74            h.new_object(m)
75        })),
76        _ => None,
77    }
78}
79
80/// `stdlib::call` entry for `https.<method>`.
81pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
82    match method {
83        "createServer" => Some(create_server(args)),
84        "request" => Some(request(args, false)),
85        "get" => Some(request(args, true)),
86        _ => None,
87    }
88}
89
90// ── shared prop helpers ──────────────────────────────────────────────────────
91
92fn get_prop(recv: &Value, key: &str) -> Option<Value> {
93    with_host(|h| match h.get(recv) {
94        Some(JsObj::Object(p)) => p.get(key).cloned(),
95        _ => None,
96    })
97}
98fn set_prop(recv: &Value, key: &str, val: Value) {
99    with_host(|h| {
100        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
101            p.insert(key.to_string(), val);
102        }
103    });
104}
105fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
106    get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
107}
108
109/// Raw bytes of a value: Buffer bytes, else its UTF-8 string form.
110fn value_bytes(v: Option<&Value>) -> Vec<u8> {
111    let Some(v) = v else { return Vec::new() };
112    let is_buffer =
113        with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
114    if is_buffer {
115        return with_host(|h| match h.get(v) {
116            Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
117                Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
118                _ => Vec::new(),
119            },
120            _ => Vec::new(),
121        });
122    }
123    with_host(|h| h.str_of(v)).into_bytes()
124}
125
126// ── server: per-connection parse state ───────────────────────────────────────
127
128struct HttpsConn {
129    listener: Value,
130    buf: Vec<u8>,
131}
132
133struct ResState {
134    sock_id: u64,
135    status: u16,
136    message: Option<String>,
137    headers: Vec<(String, String)>,
138    body: Vec<u8>,
139}
140
141thread_local! {
142    static CONNS: std::cell::RefCell<HashMap<u64, HttpsConn>> =
143        std::cell::RefCell::new(HashMap::new());
144    static RESPONSES: std::cell::RefCell<HashMap<u64, ResState>> =
145        std::cell::RefCell::new(HashMap::new());
146    static NEXT_RESID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
147    static CLIENT_REQS: std::cell::RefCell<HashMap<u64, ClientReq>> =
148        std::cell::RefCell::new(HashMap::new());
149    static NEXT_REQID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
150}
151
152fn next_resid() -> u64 {
153    NEXT_RESID.with(|c| {
154        let id = c.get();
155        c.set(id + 1);
156        id
157    })
158}
159fn next_reqid() -> u64 {
160    NEXT_REQID.with(|c| {
161        let id = c.get();
162        c.set(id + 1);
163        id
164    })
165}
166
167// ── https.createServer ───────────────────────────────────────────────────────
168
169pub fn create_server(args: &[Value]) -> Result<Value, String> {
170    let mut options: Option<Value> = None;
171    let mut listener = Value::Undef;
172    for a in args {
173        if with_host(|h| crate::host::is_callable(h, a)) {
174            listener = a.clone();
175        } else if matches!(a, Value::Obj(_)) {
176            options = Some(a.clone());
177        }
178    }
179    let opts = options.ok_or_else(|| {
180        crate::host::type_error(
181            "https.createServer requires an options object with `key` and `cert`",
182        )
183    })?;
184    let cert = value_bytes(get_prop(&opts, "cert").as_ref());
185    let key = value_bytes(get_prop(&opts, "key").as_ref());
186    if cert.is_empty() || key.is_empty() {
187        return Err(crate::host::type_error(
188            "https.createServer requires `key` and `cert`",
189        ));
190    }
191    let config = super::tls::build_server_config(&cert, &key)?;
192
193    // Per-connection hook: register the http parser for this socket id.
194    let listener_for_hook = listener.clone();
195    let hook: super::tls::ConnHook =
196        std::rc::Rc::new(move |_server: &Value, _socket: &Value, sock_id: u64| {
197            CONNS.with(|c| {
198                c.borrow_mut().insert(
199                    sock_id,
200                    HttpsConn {
201                        listener: listener_for_hook.clone(),
202                        buf: Vec::new(),
203                    },
204                );
205            });
206            Ok(())
207        });
208    Ok(super::tls::create_server_with_config(
209        config, hook, listener,
210    ))
211}
212
213/// Discard an https connection when its TLS socket closes (called by `tls`).
214pub fn drop_conn(sock_id: u64) {
215    CONNS.with(|c| {
216        c.borrow_mut().remove(&sock_id);
217    });
218}
219
220// ── server request parsing (called from tls::on_socket_data) ─────────────────
221
222/// Feed decrypted bytes into the https request parser. No-op for a socket that is
223/// not an https connection. Runs on the main thread.
224pub fn feed(sock_id: u64, _socket: &Value, bytes: &[u8]) -> Result<(), String> {
225    let is_https = CONNS.with(|c| c.borrow().contains_key(&sock_id));
226    if !is_https {
227        return Ok(());
228    }
229    CONNS.with(|c| {
230        c.borrow_mut()
231            .get_mut(&sock_id)
232            .unwrap()
233            .buf
234            .extend_from_slice(bytes)
235    });
236
237    loop {
238        let (listener, parsed) = CONNS.with(|c| {
239            let mut c = c.borrow_mut();
240            let conn = c.get_mut(&sock_id).unwrap();
241            match parse_request(&conn.buf) {
242                Some((req, consumed)) => {
243                    conn.buf.drain(..consumed);
244                    (conn.listener.clone(), Some(req))
245                }
246                None => (Value::Undef, None),
247            }
248        });
249        let Some(parsed) = parsed else { break };
250
251        let req = build_incoming(&parsed);
252        let res = build_response(sock_id);
253        if with_host(|h| crate::host::is_callable(h, &listener)) {
254            invoke(&listener, vec![req.clone(), res], None)?;
255        }
256        if !parsed.body.is_empty() {
257            let chunk = super::buffer::from_bytes(&parsed.body);
258            super::events::instance_call(
259                &req,
260                "emit",
261                vec![with_host(|h| h.new_str("data")), chunk],
262            )?;
263        }
264        super::events::instance_call(&req, "emit", vec![with_host(|h| h.new_str("end"))])?;
265    }
266    Ok(())
267}
268
269/// A fully parsed HTTP request. (Mirror of `http::ParsedReq`.)
270struct ParsedReq {
271    method: String,
272    url: String,
273    http_version: String,
274    headers: Vec<(String, String)>,
275    body: Vec<u8>,
276}
277
278/// Parse one complete request from `buf`, or `None` if more bytes are needed.
279/// (Mirror of `http::parse_request`.)
280fn parse_request(buf: &[u8]) -> Option<(ParsedReq, usize)> {
281    let head_end = find_subslice(buf, b"\r\n\r\n")?;
282    let head = &buf[..head_end];
283    let body_start = head_end + 4;
284
285    let head_str = String::from_utf8_lossy(head);
286    let mut lines = head_str.split("\r\n");
287    let request_line = lines.next()?;
288    let mut parts = request_line.split(' ');
289    let method = parts.next()?.to_string();
290    let url = parts.next()?.to_string();
291    let version = parts.next().unwrap_or("HTTP/1.1");
292    let http_version = version.strip_prefix("HTTP/").unwrap_or("1.1").to_string();
293
294    let mut headers: Vec<(String, String)> = Vec::new();
295    let mut content_length = 0usize;
296    for line in lines {
297        if line.is_empty() {
298            continue;
299        }
300        if let Some((k, v)) = line.split_once(':') {
301            let name = k.trim().to_ascii_lowercase();
302            let value = v.trim().to_string();
303            if name == "content-length" {
304                content_length = value.parse().unwrap_or(0);
305            }
306            headers.push((name, value));
307        }
308    }
309    if buf.len() < body_start + content_length {
310        return None;
311    }
312    let body = buf[body_start..body_start + content_length].to_vec();
313    Some((
314        ParsedReq {
315            method,
316            url,
317            http_version,
318            headers,
319            body,
320        },
321        body_start + content_length,
322    ))
323}
324
325fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
326    haystack.windows(needle.len()).position(|w| w == needle)
327}
328
329fn build_incoming(req: &ParsedReq) -> Value {
330    let headers_obj = with_host(|h| {
331        let mut m = IndexMap::new();
332        for (k, v) in &req.headers {
333            m.insert(k.clone(), h.new_str(v.clone()));
334        }
335        h.new_object(m)
336    });
337    let mut extra = IndexMap::new();
338    extra.insert(
339        "method".into(),
340        with_host(|h| h.new_str(req.method.clone())),
341    );
342    extra.insert("url".into(), with_host(|h| h.new_str(req.url.clone())));
343    extra.insert(
344        "httpVersion".into(),
345        with_host(|h| h.new_str(req.http_version.clone())),
346    );
347    extra.insert("headers".into(), headers_obj);
348    // Reuse http's IncomingMessage tag: only its EventEmitter surface is used, and
349    // that routes through `http::instance_call` → `events`.
350    super::tls::new_emitter_object("IncomingMessage", extra)
351}
352
353fn build_response(sock_id: u64) -> Value {
354    let resid = next_resid();
355    RESPONSES.with(|r| {
356        r.borrow_mut().insert(
357            resid,
358            ResState {
359                sock_id,
360                status: 200,
361                message: None,
362                headers: Vec::new(),
363                body: Vec::new(),
364            },
365        );
366    });
367    let mut extra = IndexMap::new();
368    extra.insert("@@resid".into(), Value::Float(resid as f64));
369    extra.insert("statusCode".into(), Value::Float(200.0));
370    super::tls::new_emitter_object("HTTPSServerResponse", extra)
371}
372
373// ── instance dispatch ────────────────────────────────────────────────────────
374
375pub fn instance_call(
376    tag: &str,
377    recv: &Value,
378    method: &str,
379    args: Vec<Value>,
380) -> Result<Value, String> {
381    if super::events::METHODS.contains(&method) {
382        return super::events::instance_call(recv, method, args);
383    }
384    match tag {
385        "HTTPSServerResponse" => response_call(recv, method, args),
386        "HTTPSClientRequest" => client_request_call(recv, method, args),
387        _ => Err(crate::host::type_error(&format!(
388            "{method} is not a function"
389        ))),
390    }
391}
392
393fn resid_of(res: &Value) -> Option<u64> {
394    u64_prop(res, "@@resid")
395}
396
397fn response_call(res: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
398    let Some(resid) = resid_of(res) else {
399        return Err(crate::host::type_error("invalid ServerResponse"));
400    };
401    match method {
402        "writeHead" => {
403            let status =
404                with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u16;
405            let mut message: Option<String> = None;
406            let mut headers_arg: Option<Value> = None;
407            if let Some(a) = args.get(1) {
408                if with_host(|h| h.as_str(a)).is_some() {
409                    message = Some(with_host(|h| h.str_of(a)));
410                } else if !matches!(a, Value::Undef) {
411                    headers_arg = Some(a.clone());
412                }
413            }
414            if let Some(a) = args.get(2) {
415                if !matches!(a, Value::Undef) {
416                    headers_arg = Some(a.clone());
417                }
418            }
419            let header_pairs = headers_arg.map(|h| object_pairs(&h)).unwrap_or_default();
420            RESPONSES.with(|r| {
421                if let Some(st) = r.borrow_mut().get_mut(&resid) {
422                    st.status = status;
423                    st.message = message;
424                    for (k, v) in header_pairs {
425                        upsert_header(&mut st.headers, &k, v);
426                    }
427                }
428            });
429            set_prop(res, "statusCode", Value::Float(status as f64));
430            Ok(res.clone())
431        }
432        "setHeader" => {
433            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
434            let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
435            RESPONSES.with(|r| {
436                if let Some(st) = r.borrow_mut().get_mut(&resid) {
437                    upsert_header(&mut st.headers, &k, v);
438                }
439            });
440            Ok(Value::Undef)
441        }
442        "getHeader" => {
443            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
444                .to_ascii_lowercase();
445            let val = RESPONSES.with(|r| {
446                r.borrow().get(&resid).and_then(|st| {
447                    st.headers
448                        .iter()
449                        .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
450                        .map(|(_, v)| v.clone())
451                })
452            });
453            Ok(val
454                .map(|v| with_host(|h| h.new_str(v)))
455                .unwrap_or(Value::Undef))
456        }
457        "removeHeader" => {
458            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
459            RESPONSES.with(|r| {
460                if let Some(st) = r.borrow_mut().get_mut(&resid) {
461                    st.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
462                }
463            });
464            Ok(Value::Undef)
465        }
466        "flushHeaders" => Ok(Value::Undef),
467        "write" => {
468            let bytes = value_bytes(args.first());
469            RESPONSES.with(|r| {
470                if let Some(st) = r.borrow_mut().get_mut(&resid) {
471                    st.body.extend_from_slice(&bytes);
472                }
473            });
474            Ok(Value::Bool(true))
475        }
476        "end" => {
477            if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
478                let bytes = value_bytes(Some(chunk));
479                RESPONSES.with(|r| {
480                    if let Some(st) = r.borrow_mut().get_mut(&resid) {
481                        st.body.extend_from_slice(&bytes);
482                    }
483                });
484            }
485            finish_response(res, resid)?;
486            Ok(res.clone())
487        }
488        _ => Err(crate::host::type_error(&format!(
489            "res.{method} is not a function"
490        ))),
491    }
492}
493
494fn finish_response(res: &Value, resid: u64) -> Result<(), String> {
495    let js_status = u64_prop(res, "statusCode").map(|n| n as u16);
496    let st = RESPONSES.with(|r| r.borrow_mut().remove(&resid));
497    let Some(mut st) = st else { return Ok(()) };
498    if let Some(s) = js_status {
499        st.status = s;
500    }
501    let payload = serialize_response(&mut st);
502    super::tls::socket_write(st.sock_id, &payload);
503    super::tls::socket_end(st.sock_id);
504    super::events::instance_call(res, "emit", vec![with_host(|h| h.new_str("finish"))])?;
505    Ok(())
506}
507
508/// Serialize the HTTP/1.1 response bytes. (Mirror of `http::serialize_response`,
509/// except the response always closes the connection — the TLS owner shuts down the
510/// write half after `res.end`.)
511fn serialize_response(st: &mut ResState) -> Vec<u8> {
512    let reason = st
513        .message
514        .clone()
515        .unwrap_or_else(|| status_text(st.status).to_string());
516    let mut out = format!("HTTP/1.1 {} {}\r\n", st.status, reason).into_bytes();
517    let has = |name: &str| st.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
518    let chunked = st.headers.iter().any(|(k, v)| {
519        k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
520    });
521    for (k, v) in &st.headers {
522        out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
523    }
524    if !chunked && !has("content-length") {
525        out.extend_from_slice(format!("Content-Length: {}\r\n", st.body.len()).as_bytes());
526    }
527    if !has("connection") {
528        out.extend_from_slice(b"Connection: close\r\n");
529    }
530    out.extend_from_slice(b"\r\n");
531    out.extend_from_slice(&st.body);
532    out
533}
534
535fn upsert_header(headers: &mut Vec<(String, String)>, name: &str, value: String) {
536    if let Some(slot) = headers
537        .iter_mut()
538        .find(|(k, _)| k.eq_ignore_ascii_case(name))
539    {
540        slot.1 = value;
541    } else {
542        headers.push((name.to_string(), value));
543    }
544}
545
546fn object_pairs(obj: &Value) -> Vec<(String, String)> {
547    with_host(|h| match h.get(obj) {
548        Some(JsObj::Object(p)) => p
549            .iter()
550            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
551            .map(|(k, v)| (k.clone(), h.str_of(v)))
552            .collect(),
553        _ => Vec::new(),
554    })
555}
556
557fn status_text(code: u16) -> &'static str {
558    for &(c, msg) in super::http::status_table() {
559        if c == code {
560            return msg;
561        }
562    }
563    "OK"
564}
565
566// ── client: https.request / https.get ────────────────────────────────────────
567
568/// State of an in-flight client request (`https.request`/`https.get`) until it is
569/// dispatched by `.end()`.
570struct ClientReq {
571    host: String,
572    port: u16,
573    servername: String,
574    reject_unauthorized: bool,
575    method: String,
576    path: String,
577    headers: Vec<(String, String)>,
578    body: Vec<u8>,
579    /// The `req` object (a `HTTPSClientRequest` emitter) for `response` events.
580    request: Value,
581    sent: bool,
582}
583
584/// `https.request(options[, cb])` / `https.get(url|options[, cb])`. Returns a
585/// `ClientRequest` (a `HTTPSClientRequest` emitter). `get` auto-sends.
586pub fn request(args: &[Value], is_get: bool) -> Result<Value, String> {
587    let mut host = "localhost".to_string();
588    let mut port: u16 = 443;
589    let mut path = "/".to_string();
590    let mut method = "GET".to_string();
591    let mut servername: Option<String> = None;
592    let mut reject_unauthorized = true;
593    let mut headers: Vec<(String, String)> = Vec::new();
594    let mut cb: Option<Value> = None;
595
596    for a in args {
597        if with_host(|h| crate::host::is_callable(h, a)) {
598            cb = Some(a.clone());
599        } else if with_host(|h| h.as_str(a)).is_some() {
600            // A URL string.
601            let url = with_host(|h| h.str_of(a));
602            parse_url(&url, &mut host, &mut port, &mut path);
603        } else if matches!(a, Value::Obj(_)) {
604            for key in ["hostname", "host"] {
605                if let Some(v) = get_prop(a, key).filter(|v| with_host(|h| h.as_str(v)).is_some()) {
606                    host = with_host(|h| h.str_of(&v));
607                }
608            }
609            if let Some(v) = get_prop(a, "port") {
610                let n = with_host(|h| h.to_number(&v));
611                if !n.is_nan() {
612                    port = n as u16;
613                }
614            }
615            if let Some(v) = get_prop(a, "path").filter(|v| with_host(|h| h.as_str(v)).is_some()) {
616                path = with_host(|h| h.str_of(&v));
617            }
618            if let Some(v) = get_prop(a, "method").filter(|v| with_host(|h| h.as_str(v)).is_some())
619            {
620                method = with_host(|h| h.str_of(&v));
621            }
622            if let Some(v) =
623                get_prop(a, "servername").filter(|v| with_host(|h| h.as_str(v)).is_some())
624            {
625                servername = Some(with_host(|h| h.str_of(&v)));
626            }
627            if let Some(v) = get_prop(a, "rejectUnauthorized") {
628                reject_unauthorized = with_host(|h| h.truthy(&v));
629            }
630            if let Some(hv) = get_prop(a, "headers").filter(|v| matches!(v, Value::Obj(_))) {
631                for (k, val) in object_pairs(&hv) {
632                    headers.push((k, val));
633                }
634            }
635        }
636    }
637    if is_get {
638        method = "GET".to_string();
639    }
640    let servername = servername.unwrap_or_else(|| host.clone());
641
642    let reqid = next_reqid();
643    let mut extra = IndexMap::new();
644    extra.insert("@@reqid".into(), Value::Float(reqid as f64));
645    extra.insert("method".into(), with_host(|h| h.new_str(method.clone())));
646    extra.insert("path".into(), with_host(|h| h.new_str(path.clone())));
647    let request = super::tls::new_emitter_object("HTTPSClientRequest", extra);
648    // The `cb` passed to `request`/`get` is registered as the `response` listener
649    // (Node semantics), so it fires exactly once when the response arrives.
650    if let Some(cb) = cb {
651        super::events::instance_call(
652            &request,
653            "on",
654            vec![with_host(|h| h.new_str("response")), cb],
655        )?;
656    }
657    CLIENT_REQS.with(|c| {
658        c.borrow_mut().insert(
659            reqid,
660            ClientReq {
661                host,
662                port,
663                servername,
664                reject_unauthorized,
665                method,
666                path,
667                headers,
668                body: Vec::new(),
669                request: request.clone(),
670                sent: false,
671            },
672        );
673    });
674    // `https.get` dispatches immediately; `https.request` waits for `.end()`.
675    if is_get {
676        dispatch_request(reqid)?;
677    }
678    Ok(request)
679}
680
681fn parse_url(url: &str, host: &mut String, port: &mut u16, path: &mut String) {
682    let rest = url.strip_prefix("https://").unwrap_or(url);
683    let (authority, p) = match rest.find('/') {
684        Some(i) => (&rest[..i], &rest[i..]),
685        None => (rest, "/"),
686    };
687    *path = if p.is_empty() {
688        "/".to_string()
689    } else {
690        p.to_string()
691    };
692    if let Some((h, port_str)) = authority.rsplit_once(':') {
693        *host = h.to_string();
694        if let Ok(n) = port_str.parse::<u16>() {
695            *port = n;
696        }
697    } else {
698        *host = authority.to_string();
699        *port = 443;
700    }
701}
702
703fn client_request_call(req: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
704    let reqid = u64_prop(req, "@@reqid");
705    match method {
706        "write" => {
707            if let Some(id) = reqid {
708                let bytes = value_bytes(args.first());
709                CLIENT_REQS.with(|c| {
710                    if let Some(r) = c.borrow_mut().get_mut(&id) {
711                        r.body.extend_from_slice(&bytes);
712                    }
713                });
714            }
715            Ok(Value::Bool(true))
716        }
717        "end" => {
718            if let Some(id) = reqid {
719                if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
720                    let bytes = value_bytes(Some(chunk));
721                    CLIENT_REQS.with(|c| {
722                        if let Some(r) = c.borrow_mut().get_mut(&id) {
723                            r.body.extend_from_slice(&bytes);
724                        }
725                    });
726                }
727                dispatch_request(id)?;
728            }
729            Ok(req.clone())
730        }
731        "setHeader" => {
732            if let Some(id) = reqid {
733                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
734                let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
735                CLIENT_REQS.with(|c| {
736                    if let Some(r) = c.borrow_mut().get_mut(&id) {
737                        upsert_header(&mut r.headers, &k, v);
738                    }
739                });
740            }
741            Ok(Value::Undef)
742        }
743        "getHeader" => {
744            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
745                .to_ascii_lowercase();
746            let val = reqid.and_then(|id| {
747                CLIENT_REQS.with(|c| {
748                    c.borrow().get(&id).and_then(|r| {
749                        r.headers
750                            .iter()
751                            .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
752                            .map(|(_, v)| v.clone())
753                    })
754                })
755            });
756            Ok(val
757                .map(|v| with_host(|h| h.new_str(v)))
758                .unwrap_or(Value::Undef))
759        }
760        "removeHeader" => {
761            if let Some(id) = reqid {
762                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
763                CLIENT_REQS.with(|c| {
764                    if let Some(r) = c.borrow_mut().get_mut(&id) {
765                        r.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
766                    }
767                });
768            }
769            Ok(Value::Undef)
770        }
771        "abort" | "destroy" | "setTimeout" => Ok(req.clone()),
772        _ => Err(crate::host::type_error(&format!(
773            "req.{method} is not a function"
774        ))),
775    }
776}
777
778/// Spawn the blocking TLS exchange for a client request. Runs on a background
779/// thread; posts the parsed response to the main thread.
780fn dispatch_request(reqid: u64) -> Result<(), String> {
781    // Take the request state out; capture only `Send` data for the thread.
782    let sent = CLIENT_REQS.with(|c| c.borrow().get(&reqid).map(|r| r.sent).unwrap_or(true));
783    if sent {
784        return Ok(());
785    }
786    CLIENT_REQS.with(|c| {
787        if let Some(r) = c.borrow_mut().get_mut(&reqid) {
788            r.sent = true;
789        }
790    });
791
792    let (host, port, servername, reject, method, path, headers, body) = CLIENT_REQS.with(|c| {
793        let b = c.borrow();
794        let r = b.get(&reqid).unwrap();
795        (
796            r.host.clone(),
797            r.port,
798            r.servername.clone(),
799            r.reject_unauthorized,
800            r.method.clone(),
801            r.path.clone(),
802            r.headers.clone(),
803            r.body.clone(),
804        )
805    });
806
807    let config = super::tls::client_config(reject);
808    let io_tx = with_host(|h| h.io_sender());
809    with_host(|h| h.incr_handle());
810
811    // Build the request bytes.
812    let mut has_host = false;
813    let mut has_len = false;
814    let mut header_block = String::new();
815    for (k, v) in &headers {
816        if k.eq_ignore_ascii_case("host") {
817            has_host = true;
818        }
819        if k.eq_ignore_ascii_case("content-length") {
820            has_len = true;
821        }
822        if k.eq_ignore_ascii_case("connection") {
823            continue; // we force `close`
824        }
825        header_block.push_str(&format!("{k}: {v}\r\n"));
826    }
827    let host_header = if port == 443 {
828        host.clone()
829    } else {
830        format!("{host}:{port}")
831    };
832    let mut request_bytes = format!("{method} {path} HTTP/1.1\r\n");
833    if !has_host {
834        request_bytes.push_str(&format!("Host: {host_header}\r\n"));
835    }
836    request_bytes.push_str(&header_block);
837    if !has_len && !body.is_empty() {
838        request_bytes.push_str(&format!("Content-Length: {}\r\n", body.len()));
839    }
840    request_bytes.push_str("Connection: close\r\n\r\n");
841    let mut wire = request_bytes.into_bytes();
842    wire.extend_from_slice(&body);
843
844    std::thread::spawn(move || {
845        let result = exchange(&host, port, &servername, config, &wire);
846        match result {
847            Ok(raw) => {
848                let _ = io_tx.send(Box::new(move || deliver_response(reqid, raw)));
849            }
850            Err(msg) => {
851                let _ = io_tx.send(Box::new(move || deliver_error(reqid, msg)));
852            }
853        }
854    });
855    Ok(())
856}
857
858/// The blocking TLS round-trip: connect, handshake, write the request, read the
859/// full response to EOF. Returns the raw response bytes.
860pub(crate) fn exchange(
861    host: &str,
862    port: u16,
863    servername: &str,
864    config: std::sync::Arc<rustls::ClientConfig>,
865    request: &[u8],
866) -> Result<Vec<u8>, String> {
867    let server_name = ServerName::try_from(servername.to_string())
868        .map_err(|_| format!("Error: tls: invalid servername '{servername}'"))?;
869    let sock = TcpStream::connect((host, port))
870        .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
871    let conn =
872        ClientConnection::new(config, server_name).map_err(|e| format!("Error: tls: {e}"))?;
873    let mut stream = StreamOwned::new(conn, sock);
874    stream
875        .write_all(request)
876        .map_err(|e| format!("Error: https write: {e}"))?;
877    stream
878        .flush()
879        .map_err(|e| format!("Error: https flush: {e}"))?;
880    let mut raw = Vec::new();
881    // Read to EOF; a clean TLS close-notify surfaces as `Ok(0)`. A peer that drops
882    // the TCP connection without close-notify yields an UnexpectedEof, which for a
883    // `Connection: close` response we treat as end-of-body.
884    let mut buf = [0u8; 16384];
885    loop {
886        match stream.read(&mut buf) {
887            Ok(0) => break,
888            Ok(n) => raw.extend_from_slice(&buf[..n]),
889            Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
890            Err(e) => {
891                if raw.is_empty() {
892                    return Err(format!("Error: https read: {e}"));
893                }
894                break;
895            }
896        }
897    }
898    Ok(raw)
899}
900
901/// Parse the raw response and emit `response` (with an `IncomingMessage`) then the
902/// body `data`/`end`. Runs on the main thread.
903fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
904    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
905    with_host(|h| h.decr_handle());
906    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
907    let Some(entry) = entry else { return Ok(()) };
908
909    let (status, message, http_version, headers, body) = parse_response(&raw);
910
911    let headers_obj = with_host(|h| {
912        let mut m = IndexMap::new();
913        for (k, v) in &headers {
914            m.insert(k.clone(), h.new_str(v.clone()));
915        }
916        h.new_object(m)
917    });
918    let mut extra = IndexMap::new();
919    extra.insert("statusCode".into(), Value::Float(status as f64));
920    extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
921    extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
922    extra.insert("headers".into(), headers_obj);
923    let res = super::tls::new_emitter_object("IncomingMessage", extra);
924
925    // Fire `response` (the `cb` from `request`/`get` was registered as a listener).
926    super::events::instance_call(
927        &entry.request,
928        "emit",
929        vec![with_host(|h| h.new_str("response")), res.clone()],
930    )?;
931    if !body.is_empty() {
932        let chunk = super::buffer::from_bytes(&body);
933        super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
934    }
935    super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
936    Ok(())
937}
938
939fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
940    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
941    with_host(|h| h.decr_handle());
942    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
943    if let Some(entry) = entry {
944        let err = with_host(|h| {
945            let mut m = IndexMap::new();
946            m.insert("message".into(), h.new_str(msg.clone()));
947            h.new_object(m)
948        });
949        super::events::instance_call(
950            &entry.request,
951            "emit",
952            vec![with_host(|h| h.new_str("error")), err],
953        )?;
954    }
955    Ok(())
956}
957
958/// Parse a raw HTTP/1.1 response into `(status, message, version, headers, body)`.
959/// Handles `Transfer-Encoding: chunked` and plain (Content-Length / to-EOF) bodies.
960fn parse_response(raw: &[u8]) -> (u16, String, String, Vec<(String, String)>, Vec<u8>) {
961    let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
962    let head = String::from_utf8_lossy(&raw[..head_end]);
963    let body_start = (head_end + 4).min(raw.len());
964    let mut lines = head.split("\r\n");
965    let status_line = lines.next().unwrap_or("");
966    let mut sp = status_line.splitn(3, ' ');
967    let version = sp
968        .next()
969        .unwrap_or("HTTP/1.1")
970        .strip_prefix("HTTP/")
971        .unwrap_or("1.1")
972        .to_string();
973    let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
974    let message = sp.next().unwrap_or("").to_string();
975
976    let mut headers: Vec<(String, String)> = Vec::new();
977    let mut chunked = false;
978    for line in lines {
979        if line.is_empty() {
980            continue;
981        }
982        if let Some((k, v)) = line.split_once(':') {
983            let name = k.trim().to_ascii_lowercase();
984            let value = v.trim().to_string();
985            if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
986                chunked = true;
987            }
988            headers.push((name, value));
989        }
990    }
991    let raw_body = &raw[body_start..];
992    let body = if chunked {
993        decode_chunked(raw_body)
994    } else {
995        raw_body.to_vec()
996    };
997    (status, message, version, headers, body)
998}
999
1000/// Decode an HTTP/1.1 chunked body (best-effort; stops at the terminating 0-chunk
1001/// or when the input is exhausted).
1002fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1003    let mut out = Vec::new();
1004    while let Some(nl) = find_subslice(data, b"\r\n") {
1005        let size_line = String::from_utf8_lossy(&data[..nl]);
1006        let size_hex = size_line.split(';').next().unwrap_or("").trim();
1007        let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1008        if size == 0 {
1009            break;
1010        }
1011        let chunk_start = nl + 2;
1012        let chunk_end = (chunk_start + size).min(data.len());
1013        out.extend_from_slice(&data[chunk_start..chunk_end]);
1014        // Advance past the chunk and its trailing CRLF.
1015        let next = chunk_end + 2;
1016        if next >= data.len() {
1017            break;
1018        }
1019        data = &data[next..];
1020    }
1021    out
1022}