1use 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
33pub const MODULE_METHODS: &[&str] = &["createServer", "request", "get"];
35
36pub 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
61pub 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
80pub 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
90fn 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
109fn 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
126struct 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
167pub 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 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
213pub fn drop_conn(sock_id: u64) {
215 CONNS.with(|c| {
216 c.borrow_mut().remove(&sock_id);
217 });
218}
219
220pub 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
269struct ParsedReq {
271 method: String,
272 url: String,
273 http_version: String,
274 headers: Vec<(String, String)>,
275 body: Vec<u8>,
276}
277
278fn 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 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
373pub 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
508fn 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
566struct 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 request: Value,
581 sent: bool,
582}
583
584pub 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 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 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 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
778fn dispatch_request(reqid: u64) -> Result<(), String> {
781 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 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; }
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
858pub(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 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
901fn 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 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
958fn 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
1000fn 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 let next = chunk_end + 2;
1016 if next >= data.len() {
1017 break;
1018 }
1019 data = &data[next..];
1020 }
1021 out
1022}