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    /// The request was a HEAD, so the response carries headers and no body
136    /// (RFC 9110 §9.3.2). Mirrors `http::ResState`.
137    head: bool,
138    status: u16,
139    message: Option<String>,
140    headers: Vec<(String, String)>,
141    body: Vec<u8>,
142}
143
144thread_local! {
145    static CONNS: std::cell::RefCell<HashMap<u64, HttpsConn>> =
146        std::cell::RefCell::new(HashMap::new());
147    static RESPONSES: std::cell::RefCell<HashMap<u64, ResState>> =
148        std::cell::RefCell::new(HashMap::new());
149    static NEXT_RESID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
150    static CLIENT_REQS: std::cell::RefCell<HashMap<u64, ClientReq>> =
151        std::cell::RefCell::new(HashMap::new());
152    static NEXT_REQID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
153}
154
155fn next_resid() -> u64 {
156    NEXT_RESID.with(|c| {
157        let id = c.get();
158        c.set(id + 1);
159        id
160    })
161}
162fn next_reqid() -> u64 {
163    NEXT_REQID.with(|c| {
164        let id = c.get();
165        c.set(id + 1);
166        id
167    })
168}
169
170// ── https.createServer ───────────────────────────────────────────────────────
171
172pub fn create_server(args: &[Value]) -> Result<Value, String> {
173    let mut options: Option<Value> = None;
174    let mut listener = Value::Undef;
175    for a in args {
176        if with_host(|h| crate::host::is_callable(h, a)) {
177            listener = a.clone();
178        } else if matches!(a, Value::Obj(_)) {
179            options = Some(a.clone());
180        }
181    }
182    let opts = options.ok_or_else(|| {
183        crate::host::type_error(
184            "https.createServer requires an options object with `key` and `cert`",
185        )
186    })?;
187    let cert = value_bytes(get_prop(&opts, "cert").as_ref());
188    let key = value_bytes(get_prop(&opts, "key").as_ref());
189    if cert.is_empty() || key.is_empty() {
190        return Err(crate::host::type_error(
191            "https.createServer requires `key` and `cert`",
192        ));
193    }
194    let config = super::tls::build_server_config(&cert, &key)?;
195
196    // Per-connection hook: register the http parser for this socket id.
197    let listener_for_hook = listener.clone();
198    let hook: super::tls::ConnHook =
199        std::rc::Rc::new(move |_server: &Value, _socket: &Value, sock_id: u64| {
200            CONNS.with(|c| {
201                c.borrow_mut().insert(
202                    sock_id,
203                    HttpsConn {
204                        listener: listener_for_hook.clone(),
205                        buf: Vec::new(),
206                    },
207                );
208            });
209            Ok(())
210        });
211    Ok(super::tls::create_server_with_config(
212        config, hook, listener,
213    ))
214}
215
216/// Discard an https connection when its TLS socket closes (called by `tls`).
217pub fn drop_conn(sock_id: u64) {
218    CONNS.with(|c| {
219        c.borrow_mut().remove(&sock_id);
220    });
221}
222
223// ── server request parsing (called from tls::on_socket_data) ─────────────────
224
225/// Feed decrypted bytes into the https request parser. No-op for a socket that is
226/// not an https connection. Runs on the main thread.
227pub fn feed(sock_id: u64, _socket: &Value, bytes: &[u8]) -> Result<(), String> {
228    let is_https = CONNS.with(|c| c.borrow().contains_key(&sock_id));
229    if !is_https {
230        return Ok(());
231    }
232    CONNS.with(|c| {
233        c.borrow_mut()
234            .get_mut(&sock_id)
235            .unwrap()
236            .buf
237            .extend_from_slice(bytes)
238    });
239
240    loop {
241        let (listener, parsed) = CONNS.with(|c| {
242            let mut c = c.borrow_mut();
243            let conn = c.get_mut(&sock_id).unwrap();
244            match parse_request(&conn.buf) {
245                Some((req, consumed)) => {
246                    conn.buf.drain(..consumed);
247                    (conn.listener.clone(), Some(req))
248                }
249                None => (Value::Undef, None),
250            }
251        });
252        let Some(parsed) = parsed else { break };
253
254        let req = build_incoming(&parsed);
255        let res = build_response(sock_id, parsed.method.eq_ignore_ascii_case("HEAD"));
256        if with_host(|h| crate::host::is_callable(h, &listener)) {
257            invoke(&listener, vec![req.clone(), res], None)?;
258        }
259        // A listener that called `req.setEncoding(enc)` gets a decoded string
260        // chunk instead of a Buffer, matching Node's Readable — and matching
261        // `http::feed`, which has done this all along. Without it
262        // `setEncoding` was accepted over TLS and then ignored: the same
263        // handler saw a string over http and a Buffer over https.
264        if !parsed.body.is_empty() {
265            let encoding = with_host(|h| match h.get(&req) {
266                Some(JsObj::Object(p)) => p.get("@@encoding").map(|v| h.str_of(v)),
267                _ => None,
268            });
269            let chunk = match encoding {
270                Some(enc) => with_host(|h| {
271                    let s = super::buffer::encode_bytes(&parsed.body, &enc);
272                    h.new_str(s)
273                }),
274                None => super::buffer::from_bytes(&parsed.body),
275            };
276            super::events::instance_call(
277                &req,
278                "emit",
279                vec![with_host(|h| h.new_str("data")), chunk],
280            )?;
281        }
282        super::events::instance_call(&req, "emit", vec![with_host(|h| h.new_str("end"))])?;
283    }
284    Ok(())
285}
286
287/// A fully parsed HTTP request. (Mirror of `http::ParsedReq`.)
288struct ParsedReq {
289    method: String,
290    url: String,
291    http_version: String,
292    headers: Vec<(String, String)>,
293    body: Vec<u8>,
294}
295
296/// Parse one complete request from `buf`, or `None` if more bytes are needed.
297/// (Mirror of `http::parse_request`.)
298fn parse_request(buf: &[u8]) -> Option<(ParsedReq, usize)> {
299    let head_end = find_subslice(buf, b"\r\n\r\n")?;
300    let head = &buf[..head_end];
301    let body_start = head_end + 4;
302
303    let head_str = String::from_utf8_lossy(head);
304    let mut lines = head_str.split("\r\n");
305    let request_line = lines.next()?;
306    let mut parts = request_line.split(' ');
307    let method = parts.next()?.to_string();
308    let url = parts.next()?.to_string();
309    let version = parts.next().unwrap_or("HTTP/1.1");
310    let http_version = version.strip_prefix("HTTP/").unwrap_or("1.1").to_string();
311
312    let mut headers: Vec<(String, String)> = Vec::new();
313    let mut content_length = 0usize;
314    for line in lines {
315        if line.is_empty() {
316            continue;
317        }
318        if let Some((k, v)) = line.split_once(':') {
319            let name = k.trim().to_ascii_lowercase();
320            let value = v.trim().to_string();
321            if name == "content-length" {
322                content_length = value.parse().unwrap_or(0);
323            }
324            headers.push((name, value));
325        }
326    }
327    if buf.len() < body_start + content_length {
328        return None;
329    }
330    let body = buf[body_start..body_start + content_length].to_vec();
331    Some((
332        ParsedReq {
333            method,
334            url,
335            http_version,
336            headers,
337            body,
338        },
339        body_start + content_length,
340    ))
341}
342
343fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
344    haystack.windows(needle.len()).position(|w| w == needle)
345}
346
347fn build_incoming(req: &ParsedReq) -> Value {
348    let headers_obj = with_host(|h| {
349        let mut m = IndexMap::new();
350        for (k, v) in &req.headers {
351            m.insert(k.clone(), h.new_str(v.clone()));
352        }
353        h.new_object(m)
354    });
355    let mut extra = IndexMap::new();
356    extra.insert(
357        "method".into(),
358        with_host(|h| h.new_str(req.method.clone())),
359    );
360    extra.insert("url".into(), with_host(|h| h.new_str(req.url.clone())));
361    extra.insert(
362        "httpVersion".into(),
363        with_host(|h| h.new_str(req.http_version.clone())),
364    );
365    extra.insert("headers".into(), headers_obj);
366    // Reuse http's IncomingMessage tag: only its EventEmitter surface is used, and
367    // that routes through `http::instance_call` → `events`.
368    super::tls::new_emitter_object("IncomingMessage", extra)
369}
370
371fn build_response(sock_id: u64, head: bool) -> Value {
372    let resid = next_resid();
373    RESPONSES.with(|r| {
374        r.borrow_mut().insert(
375            resid,
376            ResState {
377                sock_id,
378                head,
379                status: 200,
380                message: None,
381                headers: Vec::new(),
382                body: Vec::new(),
383            },
384        );
385    });
386    let mut extra = IndexMap::new();
387    extra.insert("@@resid".into(), Value::Float(resid as f64));
388    extra.insert("statusCode".into(), Value::Float(200.0));
389    super::tls::new_emitter_object("HTTPSServerResponse", extra)
390}
391
392// ── instance dispatch ────────────────────────────────────────────────────────
393
394pub fn instance_call(
395    tag: &str,
396    recv: &Value,
397    method: &str,
398    args: Vec<Value>,
399) -> Result<Value, String> {
400    if super::events::METHODS.contains(&method) {
401        return super::events::instance_call(recv, method, args);
402    }
403    match tag {
404        "HTTPSServerResponse" => response_call(recv, method, args),
405        "HTTPSClientRequest" => client_request_call(recv, method, args),
406        _ => Err(crate::host::type_error(&format!(
407            "{method} is not a function"
408        ))),
409    }
410}
411
412fn resid_of(res: &Value) -> Option<u64> {
413    u64_prop(res, "@@resid")
414}
415
416fn response_call(res: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
417    let Some(resid) = resid_of(res) else {
418        return Err(crate::host::type_error("invalid ServerResponse"));
419    };
420    match method {
421        "writeHead" => {
422            let status =
423                with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u16;
424            let mut message: Option<String> = None;
425            let mut headers_arg: Option<Value> = None;
426            if let Some(a) = args.get(1) {
427                if with_host(|h| h.as_str(a)).is_some() {
428                    message = Some(with_host(|h| h.str_of(a)));
429                } else if !matches!(a, Value::Undef) {
430                    headers_arg = Some(a.clone());
431                }
432            }
433            if let Some(a) = args.get(2) {
434                if !matches!(a, Value::Undef) {
435                    headers_arg = Some(a.clone());
436                }
437            }
438            let header_pairs = headers_arg.map(|h| object_pairs(&h)).unwrap_or_default();
439            RESPONSES.with(|r| {
440                if let Some(st) = r.borrow_mut().get_mut(&resid) {
441                    st.status = status;
442                    st.message = message;
443                    for (k, v) in header_pairs {
444                        upsert_header(&mut st.headers, &k, v);
445                    }
446                }
447            });
448            set_prop(res, "statusCode", Value::Float(status as f64));
449            Ok(res.clone())
450        }
451        "setHeader" => {
452            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
453            let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
454            RESPONSES.with(|r| {
455                if let Some(st) = r.borrow_mut().get_mut(&resid) {
456                    upsert_header(&mut st.headers, &k, v);
457                }
458            });
459            Ok(Value::Undef)
460        }
461        "getHeader" => {
462            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
463                .to_ascii_lowercase();
464            let val = RESPONSES.with(|r| {
465                r.borrow().get(&resid).and_then(|st| {
466                    st.headers
467                        .iter()
468                        .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
469                        .map(|(_, v)| v.clone())
470                })
471            });
472            Ok(val
473                .map(|v| with_host(|h| h.new_str(v)))
474                .unwrap_or(Value::Undef))
475        }
476        "removeHeader" => {
477            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
478            RESPONSES.with(|r| {
479                if let Some(st) = r.borrow_mut().get_mut(&resid) {
480                    st.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
481                }
482            });
483            Ok(Value::Undef)
484        }
485        // Advertised in `stdlib::instance_methods` for ServerResponse but never
486        // implemented in either module, so all three threw "is not a function"
487        // on both protocols despite feature-detection saying they exist.
488        // Node lowercases the names it reports, so these do too, while
489        // `setHeader` keeps the caller's spelling on the wire.
490        "hasHeader" => {
491            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
492            let found = RESPONSES.with(|r| {
493                r.borrow()
494                    .get(&resid)
495                    .is_some_and(|st| st.headers.iter().any(|(hk, _)| hk.eq_ignore_ascii_case(&k)))
496            });
497            Ok(Value::Bool(found))
498        }
499        "getHeaderNames" => {
500            let names = RESPONSES.with(|r| {
501                r.borrow()
502                    .get(&resid)
503                    .map(|st| {
504                        st.headers
505                            .iter()
506                            .map(|(k, _)| k.to_ascii_lowercase())
507                            .collect::<Vec<_>>()
508                    })
509                    .unwrap_or_default()
510            });
511            Ok(with_host(|h| {
512                let items = names.into_iter().map(|n| h.new_str(n)).collect::<Vec<_>>();
513                h.new_array(items)
514            }))
515        }
516        "getHeaders" => {
517            let pairs = RESPONSES.with(|r| {
518                r.borrow()
519                    .get(&resid)
520                    .map(|st| {
521                        st.headers
522                            .iter()
523                            .map(|(k, v)| (k.to_ascii_lowercase(), v.clone()))
524                            .collect::<Vec<_>>()
525                    })
526                    .unwrap_or_default()
527            });
528            Ok(with_host(|h| {
529                let mut m = IndexMap::new();
530                for (k, v) in pairs {
531                    m.insert(k, h.new_str(v));
532                }
533                h.new_object(m)
534            }))
535        }
536        "flushHeaders" => Ok(Value::Undef),
537        "write" => {
538            let bytes = value_bytes(args.first());
539            RESPONSES.with(|r| {
540                if let Some(st) = r.borrow_mut().get_mut(&resid) {
541                    st.body.extend_from_slice(&bytes);
542                }
543            });
544            Ok(Value::Bool(true))
545        }
546        "end" => {
547            if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
548                let bytes = value_bytes(Some(chunk));
549                RESPONSES.with(|r| {
550                    if let Some(st) = r.borrow_mut().get_mut(&resid) {
551                        st.body.extend_from_slice(&bytes);
552                    }
553                });
554            }
555            finish_response(res, resid)?;
556            Ok(res.clone())
557        }
558        _ => Err(crate::host::type_error(&format!(
559            "res.{method} is not a function"
560        ))),
561    }
562}
563
564fn finish_response(res: &Value, resid: u64) -> Result<(), String> {
565    let js_status = u64_prop(res, "statusCode").map(|n| n as u16);
566    let st = RESPONSES.with(|r| r.borrow_mut().remove(&resid));
567    let Some(mut st) = st else { return Ok(()) };
568    if let Some(s) = js_status {
569        st.status = s;
570    }
571    let payload = serialize_response(&mut st);
572    super::tls::socket_write(st.sock_id, &payload);
573    super::tls::socket_end(st.sock_id);
574    super::events::instance_call(res, "emit", vec![with_host(|h| h.new_str("finish"))])?;
575    Ok(())
576}
577
578/// Serialize the HTTP/1.1 response bytes. (Mirror of `http::serialize_response`,
579/// except the response always closes the connection — the TLS owner shuts down the
580/// write half after `res.end`.)
581fn serialize_response(st: &mut ResState) -> Vec<u8> {
582    let reason = st
583        .message
584        .clone()
585        .unwrap_or_else(|| status_text(st.status).to_string());
586    let mut out = format!("HTTP/1.1 {} {}\r\n", st.status, reason).into_bytes();
587    let has = |name: &str| st.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
588    let chunked = st.headers.iter().any(|(k, v)| {
589        k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
590    });
591    for (k, v) in &st.headers {
592        out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
593    }
594    if !chunked && !has("content-length") {
595        out.extend_from_slice(format!("Content-Length: {}\r\n", st.body.len()).as_bytes());
596    }
597    if !has("connection") {
598        out.extend_from_slice(b"Connection: close\r\n");
599    }
600    out.extend_from_slice(b"\r\n");
601    // Same two rules as `http::serialize_response`, which this is otherwise a
602    // copy of. Both were missing here and both were worse over TLS: this server
603    // sends `Connection: close`, so a client reads to EOF and then hands the
604    // raw bytes to the chunked decoder, which finds no valid chunk header and
605    // yields NOTHING — a chunked body was lost entirely rather than merely
606    // mis-framed. HEAD likewise returned the full body, against RFC 9110
607    // §9.3.2.
608    if st.head {
609        // Headers only; `Content-Length` above still describes the GET body.
610    } else if chunked {
611        if !st.body.is_empty() {
612            out.extend_from_slice(format!("{:x}\r\n", st.body.len()).as_bytes());
613            out.extend_from_slice(&st.body);
614            out.extend_from_slice(b"\r\n");
615        }
616        out.extend_from_slice(b"0\r\n\r\n");
617    } else {
618        out.extend_from_slice(&st.body);
619    }
620    out
621}
622
623fn upsert_header(headers: &mut Vec<(String, String)>, name: &str, value: String) {
624    if let Some(slot) = headers
625        .iter_mut()
626        .find(|(k, _)| k.eq_ignore_ascii_case(name))
627    {
628        slot.1 = value;
629    } else {
630        headers.push((name.to_string(), value));
631    }
632}
633
634fn object_pairs(obj: &Value) -> Vec<(String, String)> {
635    with_host(|h| match h.get(obj) {
636        Some(JsObj::Object(p)) => p
637            .iter()
638            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
639            .map(|(k, v)| (k.clone(), h.str_of(v)))
640            .collect(),
641        _ => Vec::new(),
642    })
643}
644
645fn status_text(code: u16) -> &'static str {
646    for &(c, msg) in super::http::status_table() {
647        if c == code {
648            return msg;
649        }
650    }
651    "OK"
652}
653
654// ── client: https.request / https.get ────────────────────────────────────────
655
656/// State of an in-flight client request (`https.request`/`https.get`) until it is
657/// dispatched by `.end()`.
658struct ClientReq {
659    host: String,
660    port: u16,
661    servername: String,
662    reject_unauthorized: bool,
663    method: String,
664    path: String,
665    headers: Vec<(String, String)>,
666    body: Vec<u8>,
667    /// The `req` object (a `HTTPSClientRequest` emitter) for `response` events.
668    request: Value,
669    sent: bool,
670}
671
672/// `https.request(options[, cb])` / `https.get(url|options[, cb])`. Returns a
673/// `ClientRequest` (a `HTTPSClientRequest` emitter). `get` auto-sends.
674pub fn request(args: &[Value], is_get: bool) -> Result<Value, String> {
675    let mut host = "localhost".to_string();
676    let mut port: u16 = 443;
677    let mut path = "/".to_string();
678    let mut method = "GET".to_string();
679    let mut servername: Option<String> = None;
680    let mut reject_unauthorized = true;
681    let mut headers: Vec<(String, String)> = Vec::new();
682    let mut cb: Option<Value> = None;
683
684    for a in args {
685        if with_host(|h| crate::host::is_callable(h, a)) {
686            cb = Some(a.clone());
687        } else if with_host(|h| h.as_str(a)).is_some() {
688            // A URL string.
689            let url = with_host(|h| h.str_of(a));
690            parse_url(&url, &mut host, &mut port, &mut path);
691        } else if matches!(a, Value::Obj(_)) {
692            for key in ["hostname", "host"] {
693                if let Some(v) = get_prop(a, key).filter(|v| with_host(|h| h.as_str(v)).is_some()) {
694                    host = with_host(|h| h.str_of(&v));
695                }
696            }
697            if let Some(v) = get_prop(a, "port") {
698                let n = with_host(|h| h.to_number(&v));
699                if !n.is_nan() {
700                    port = n as u16;
701                }
702            }
703            if let Some(v) = get_prop(a, "path").filter(|v| with_host(|h| h.as_str(v)).is_some()) {
704                path = with_host(|h| h.str_of(&v));
705            }
706            if let Some(v) = get_prop(a, "method").filter(|v| with_host(|h| h.as_str(v)).is_some())
707            {
708                method = with_host(|h| h.str_of(&v));
709            }
710            if let Some(v) =
711                get_prop(a, "servername").filter(|v| with_host(|h| h.as_str(v)).is_some())
712            {
713                servername = Some(with_host(|h| h.str_of(&v)));
714            }
715            if let Some(v) = get_prop(a, "rejectUnauthorized") {
716                reject_unauthorized = with_host(|h| h.truthy(&v));
717            }
718            if let Some(hv) = get_prop(a, "headers").filter(|v| matches!(v, Value::Obj(_))) {
719                for (k, val) in object_pairs(&hv) {
720                    headers.push((k, val));
721                }
722            }
723        }
724    }
725    if is_get {
726        method = "GET".to_string();
727    }
728    let servername = servername.unwrap_or_else(|| host.clone());
729
730    let reqid = next_reqid();
731    let mut extra = IndexMap::new();
732    extra.insert("@@reqid".into(), Value::Float(reqid as f64));
733    extra.insert("method".into(), with_host(|h| h.new_str(method.clone())));
734    extra.insert("path".into(), with_host(|h| h.new_str(path.clone())));
735    let request = super::tls::new_emitter_object("HTTPSClientRequest", extra);
736    // The `cb` passed to `request`/`get` is registered as the `response` listener
737    // (Node semantics), so it fires exactly once when the response arrives.
738    if let Some(cb) = cb {
739        super::events::instance_call(
740            &request,
741            "on",
742            vec![with_host(|h| h.new_str("response")), cb],
743        )?;
744    }
745    CLIENT_REQS.with(|c| {
746        c.borrow_mut().insert(
747            reqid,
748            ClientReq {
749                host,
750                port,
751                servername,
752                reject_unauthorized,
753                method,
754                path,
755                headers,
756                body: Vec::new(),
757                request: request.clone(),
758                sent: false,
759            },
760        );
761    });
762    // `https.get` dispatches immediately; `https.request` waits for `.end()`.
763    if is_get {
764        dispatch_request(reqid)?;
765    }
766    Ok(request)
767}
768
769fn parse_url(url: &str, host: &mut String, port: &mut u16, path: &mut String) {
770    let rest = url.strip_prefix("https://").unwrap_or(url);
771    let (authority, p) = match rest.find('/') {
772        Some(i) => (&rest[..i], &rest[i..]),
773        None => (rest, "/"),
774    };
775    *path = if p.is_empty() {
776        "/".to_string()
777    } else {
778        p.to_string()
779    };
780    if let Some((h, port_str)) = authority.rsplit_once(':') {
781        *host = h.to_string();
782        if let Ok(n) = port_str.parse::<u16>() {
783            *port = n;
784        }
785    } else {
786        *host = authority.to_string();
787        *port = 443;
788    }
789}
790
791fn client_request_call(req: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
792    let reqid = u64_prop(req, "@@reqid");
793    match method {
794        "write" => {
795            if let Some(id) = reqid {
796                let bytes = value_bytes(args.first());
797                CLIENT_REQS.with(|c| {
798                    if let Some(r) = c.borrow_mut().get_mut(&id) {
799                        r.body.extend_from_slice(&bytes);
800                    }
801                });
802            }
803            Ok(Value::Bool(true))
804        }
805        "end" => {
806            if let Some(id) = reqid {
807                if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
808                    let bytes = value_bytes(Some(chunk));
809                    CLIENT_REQS.with(|c| {
810                        if let Some(r) = c.borrow_mut().get_mut(&id) {
811                            r.body.extend_from_slice(&bytes);
812                        }
813                    });
814                }
815                dispatch_request(id)?;
816            }
817            Ok(req.clone())
818        }
819        "setHeader" => {
820            if let Some(id) = reqid {
821                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
822                let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
823                CLIENT_REQS.with(|c| {
824                    if let Some(r) = c.borrow_mut().get_mut(&id) {
825                        upsert_header(&mut r.headers, &k, v);
826                    }
827                });
828            }
829            Ok(Value::Undef)
830        }
831        "getHeader" => {
832            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
833                .to_ascii_lowercase();
834            let val = reqid.and_then(|id| {
835                CLIENT_REQS.with(|c| {
836                    c.borrow().get(&id).and_then(|r| {
837                        r.headers
838                            .iter()
839                            .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
840                            .map(|(_, v)| v.clone())
841                    })
842                })
843            });
844            Ok(val
845                .map(|v| with_host(|h| h.new_str(v)))
846                .unwrap_or(Value::Undef))
847        }
848        "removeHeader" => {
849            if let Some(id) = reqid {
850                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
851                CLIENT_REQS.with(|c| {
852                    if let Some(r) = c.borrow_mut().get_mut(&id) {
853                        r.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
854                    }
855                });
856            }
857            Ok(Value::Undef)
858        }
859        "abort" | "destroy" | "setTimeout" => Ok(req.clone()),
860        _ => Err(crate::host::type_error(&format!(
861            "req.{method} is not a function"
862        ))),
863    }
864}
865
866/// Spawn the blocking TLS exchange for a client request. Runs on a background
867/// thread; posts the parsed response to the main thread.
868fn dispatch_request(reqid: u64) -> Result<(), String> {
869    // Take the request state out; capture only `Send` data for the thread.
870    let sent = CLIENT_REQS.with(|c| c.borrow().get(&reqid).map(|r| r.sent).unwrap_or(true));
871    if sent {
872        return Ok(());
873    }
874    CLIENT_REQS.with(|c| {
875        if let Some(r) = c.borrow_mut().get_mut(&reqid) {
876            r.sent = true;
877        }
878    });
879
880    let (host, port, servername, reject, method, path, headers, body) = CLIENT_REQS.with(|c| {
881        let b = c.borrow();
882        let r = b.get(&reqid).unwrap();
883        (
884            r.host.clone(),
885            r.port,
886            r.servername.clone(),
887            r.reject_unauthorized,
888            r.method.clone(),
889            r.path.clone(),
890            r.headers.clone(),
891            r.body.clone(),
892        )
893    });
894
895    let config = super::tls::client_config(reject);
896    let io_tx = with_host(|h| h.io_sender());
897    with_host(|h| h.incr_handle());
898
899    // Build the request bytes.
900    let mut has_host = false;
901    let mut has_len = false;
902    let mut header_block = String::new();
903    for (k, v) in &headers {
904        if k.eq_ignore_ascii_case("host") {
905            has_host = true;
906        }
907        if k.eq_ignore_ascii_case("content-length") {
908            has_len = true;
909        }
910        if k.eq_ignore_ascii_case("connection") {
911            continue; // we force `close`
912        }
913        header_block.push_str(&format!("{k}: {v}\r\n"));
914    }
915    let host_header = if port == 443 {
916        host.clone()
917    } else {
918        format!("{host}:{port}")
919    };
920    let mut request_bytes = format!("{method} {path} HTTP/1.1\r\n");
921    if !has_host {
922        request_bytes.push_str(&format!("Host: {host_header}\r\n"));
923    }
924    request_bytes.push_str(&header_block);
925    if !has_len && !body.is_empty() {
926        request_bytes.push_str(&format!("Content-Length: {}\r\n", body.len()));
927    }
928    request_bytes.push_str("Connection: close\r\n\r\n");
929    let mut wire = request_bytes.into_bytes();
930    wire.extend_from_slice(&body);
931
932    std::thread::spawn(move || {
933        let result = exchange(&host, port, &servername, config, &wire);
934        match result {
935            Ok(raw) => {
936                let _ = io_tx.send(Box::new(move || deliver_response(reqid, raw)));
937            }
938            Err(msg) => {
939                let _ = io_tx.send(Box::new(move || deliver_error(reqid, msg)));
940            }
941        }
942    });
943    Ok(())
944}
945
946/// The blocking TLS round-trip: connect, handshake, write the request, read the
947/// full response to EOF. Returns the raw response bytes.
948pub(crate) fn exchange(
949    host: &str,
950    port: u16,
951    servername: &str,
952    config: std::sync::Arc<rustls::ClientConfig>,
953    request: &[u8],
954) -> Result<Vec<u8>, String> {
955    let server_name = ServerName::try_from(servername.to_string())
956        .map_err(|_| format!("Error: tls: invalid servername '{servername}'"))?;
957    let sock = TcpStream::connect((host, port))
958        .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
959    let conn =
960        ClientConnection::new(config, server_name).map_err(|e| format!("Error: tls: {e}"))?;
961    let mut stream = StreamOwned::new(conn, sock);
962    stream
963        .write_all(request)
964        .map_err(|e| format!("Error: https write: {e}"))?;
965    stream
966        .flush()
967        .map_err(|e| format!("Error: https flush: {e}"))?;
968    let head_request = request
969        .split(|b| *b == b' ')
970        .next()
971        .is_some_and(|m| m.eq_ignore_ascii_case(b"HEAD"));
972    let mut raw = Vec::new();
973    // Stop when the response is COMPLETE by its own framing, exactly as
974    // `http::exchange` does — see the note there. Reading to EOF worked only
975    // because THIS crate's server answers `Connection: close`; against a
976    // keep-alive server, which is every real one, the read never returned and
977    // `https.request` hung forever. Verified against a TLS server that sends
978    // `Content-Length` and holds the connection open: it hung, and now it does
979    // not.
980    //
981    // EOF still ends the read, for a response with no framing at all: a clean
982    // TLS close-notify surfaces as `Ok(0)`, and a peer that drops the TCP
983    // connection without one yields `UnexpectedEof`.
984    let mut buf = [0u8; 16384];
985    loop {
986        if super::http::response_is_complete(&raw, head_request) {
987            break;
988        }
989        match stream.read(&mut buf) {
990            Ok(0) => break,
991            Ok(n) => raw.extend_from_slice(&buf[..n]),
992            Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
993            Err(e) => {
994                if raw.is_empty() {
995                    return Err(format!("Error: https read: {e}"));
996                }
997                break;
998            }
999        }
1000    }
1001    Ok(raw)
1002}
1003
1004/// Parse the raw response and emit `response` (with an `IncomingMessage`) then the
1005/// body `data`/`end`. Runs on the main thread.
1006fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
1007    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1008    with_host(|h| h.decr_handle());
1009    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1010    let Some(entry) = entry else { return Ok(()) };
1011
1012    let (status, message, http_version, headers, body) = parse_response(&raw);
1013
1014    let headers_obj = with_host(|h| {
1015        let mut m = IndexMap::new();
1016        for (k, v) in &headers {
1017            m.insert(k.clone(), h.new_str(v.clone()));
1018        }
1019        h.new_object(m)
1020    });
1021    let mut extra = IndexMap::new();
1022    extra.insert("statusCode".into(), Value::Float(status as f64));
1023    extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
1024    extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
1025    extra.insert("headers".into(), headers_obj);
1026    let res = super::tls::new_emitter_object("IncomingMessage", extra);
1027
1028    // Fire `response` (the `cb` from `request`/`get` was registered as a listener).
1029    super::events::instance_call(
1030        &entry.request,
1031        "emit",
1032        vec![with_host(|h| h.new_str("response")), res.clone()],
1033    )?;
1034    if !body.is_empty() {
1035        let chunk = super::buffer::from_bytes(&body);
1036        super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
1037    }
1038    super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
1039    Ok(())
1040}
1041
1042fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
1043    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1044    with_host(|h| h.decr_handle());
1045    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1046    if let Some(entry) = entry {
1047        let err = with_host(|h| {
1048            let mut m = IndexMap::new();
1049            m.insert("message".into(), h.new_str(msg.clone()));
1050            h.new_object(m)
1051        });
1052        super::events::instance_call(
1053            &entry.request,
1054            "emit",
1055            vec![with_host(|h| h.new_str("error")), err],
1056        )?;
1057    }
1058    Ok(())
1059}
1060
1061/// Parse a raw HTTP/1.1 response into `(status, message, version, headers, body)`.
1062/// Handles `Transfer-Encoding: chunked` and plain (Content-Length / to-EOF) bodies.
1063fn parse_response(raw: &[u8]) -> (u16, String, String, Vec<(String, String)>, Vec<u8>) {
1064    let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
1065    let head = String::from_utf8_lossy(&raw[..head_end]);
1066    let body_start = (head_end + 4).min(raw.len());
1067    let mut lines = head.split("\r\n");
1068    let status_line = lines.next().unwrap_or("");
1069    let mut sp = status_line.splitn(3, ' ');
1070    let version = sp
1071        .next()
1072        .unwrap_or("HTTP/1.1")
1073        .strip_prefix("HTTP/")
1074        .unwrap_or("1.1")
1075        .to_string();
1076    let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
1077    let message = sp.next().unwrap_or("").to_string();
1078
1079    let mut headers: Vec<(String, String)> = Vec::new();
1080    let mut chunked = false;
1081    for line in lines {
1082        if line.is_empty() {
1083            continue;
1084        }
1085        if let Some((k, v)) = line.split_once(':') {
1086            let name = k.trim().to_ascii_lowercase();
1087            let value = v.trim().to_string();
1088            if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1089                chunked = true;
1090            }
1091            headers.push((name, value));
1092        }
1093    }
1094    let raw_body = &raw[body_start..];
1095    let body = if chunked {
1096        decode_chunked(raw_body)
1097    } else {
1098        raw_body.to_vec()
1099    };
1100    (status, message, version, headers, body)
1101}
1102
1103/// Decode an HTTP/1.1 chunked body (best-effort; stops at the terminating 0-chunk
1104/// or when the input is exhausted).
1105fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1106    let mut out = Vec::new();
1107    while let Some(nl) = find_subslice(data, b"\r\n") {
1108        let size_line = String::from_utf8_lossy(&data[..nl]);
1109        let size_hex = size_line.split(';').next().unwrap_or("").trim();
1110        let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1111        if size == 0 {
1112            break;
1113        }
1114        let chunk_start = nl + 2;
1115        let chunk_end = (chunk_start + size).min(data.len());
1116        out.extend_from_slice(&data[chunk_start..chunk_end]);
1117        // Advance past the chunk and its trailing CRLF.
1118        let next = chunk_end + 2;
1119        if next >= data.len() {
1120            break;
1121        }
1122        data = &data[next..];
1123    }
1124    out
1125}