1use crate::host::{invoke, with_host, JsObj};
13use fusevm::Value;
14use indexmap::IndexMap;
15use std::collections::HashMap;
16use std::io::{Read, Write};
17use std::net::TcpStream;
18
19pub const MODULE_METHODS: &[&str] = &[
21 "createServer",
22 "request",
23 "get",
24 "validateHeaderName",
25 "validateHeaderValue",
26 "setMaxIdleHTTPParsers",
27 "setGlobalProxyFromEnv",
28];
29
30pub const CLIENT_REQUEST_METHODS: &[&str] = &[
33 "write",
34 "end",
35 "setHeader",
36 "getHeader",
37 "removeHeader",
38 "abort",
39 "destroy",
40 "setTimeout",
41 "flushHeaders",
42];
43
44const METHODS: &[&str] = &[
47 "ACL",
48 "BIND",
49 "CHECKOUT",
50 "CONNECT",
51 "COPY",
52 "DELETE",
53 "GET",
54 "HEAD",
55 "LINK",
56 "LOCK",
57 "M-SEARCH",
58 "MERGE",
59 "MKACTIVITY",
60 "MKCALENDAR",
61 "MKCOL",
62 "MOVE",
63 "NOTIFY",
64 "OPTIONS",
65 "PATCH",
66 "POST",
67 "PROPFIND",
68 "PROPPATCH",
69 "PURGE",
70 "PUT",
71 "QUERY",
72 "REBIND",
73 "REPORT",
74 "SEARCH",
75 "SOURCE",
76 "SUBSCRIBE",
77 "TRACE",
78 "UNBIND",
79 "UNLINK",
80 "UNLOCK",
81 "UNSUBSCRIBE",
82];
83
84pub fn constant(name: &str) -> Option<Value> {
87 match name {
88 "METHODS" => Some(with_host(|h| {
89 let items = METHODS.iter().map(|m| h.new_str(*m)).collect();
90 h.new_array(items)
91 })),
92 "STATUS_CODES" => Some(with_host(|h| {
93 let mut m = IndexMap::new();
94 for (code, msg) in crate::stdlib::http::status_table() {
95 m.insert(code.to_string(), h.new_str(*msg));
96 }
97 h.new_object(m)
98 })),
99 "IncomingMessage" => Some(with_host(|h| {
103 h.alloc(JsObj::Builtin("IncomingMessage".into()))
104 })),
105 "ServerResponse" => Some(with_host(|h| {
106 h.alloc(JsObj::Builtin("ServerResponse".into()))
107 })),
108 "Agent" => Some(with_host(|h| h.alloc(JsObj::Builtin("Agent".into())))),
113 "Server" => Some(with_host(|h| h.alloc(JsObj::Builtin("http.Server".into())))),
114 "ClientRequest" => Some(with_host(|h| {
115 h.alloc(JsObj::Builtin("ClientRequest".into()))
116 })),
117 "OutgoingMessage" => Some(with_host(|h| {
118 h.alloc(JsObj::Builtin("OutgoingMessage".into()))
119 })),
120 "globalAgent" => Some(construct_agent(&[])),
121 _ => None,
122 }
123}
124
125pub fn construct(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
127 match name {
128 "Agent" => Some(Ok(construct_agent(args))),
129 "http.Server" => Some(Ok(create_server(
131 args.first()
132 .cloned()
133 .filter(|v| with_host(|h| crate::host::is_callable(h, v))),
134 ))),
135 _ => None,
136 }
137}
138
139pub fn status_table() -> &'static [(u16, &'static str)] {
142 &[
143 (200, "OK"),
144 (201, "Created"),
145 (202, "Accepted"),
146 (204, "No Content"),
147 (301, "Moved Permanently"),
148 (302, "Found"),
149 (303, "See Other"),
150 (304, "Not Modified"),
151 (307, "Temporary Redirect"),
152 (308, "Permanent Redirect"),
153 (400, "Bad Request"),
154 (401, "Unauthorized"),
155 (403, "Forbidden"),
156 (404, "Not Found"),
157 (405, "Method Not Allowed"),
158 (406, "Not Acceptable"),
159 (409, "Conflict"),
160 (410, "Gone"),
161 (411, "Length Required"),
162 (413, "Payload Too Large"),
163 (414, "URI Too Long"),
164 (415, "Unsupported Media Type"),
165 (422, "Unprocessable Entity"),
166 (429, "Too Many Requests"),
167 (500, "Internal Server Error"),
168 (501, "Not Implemented"),
169 (502, "Bad Gateway"),
170 (503, "Service Unavailable"),
171 (504, "Gateway Timeout"),
172 ]
173}
174
175struct HttpConn {
179 socket: Value,
181 listener: Value,
183 buf: Vec<u8>,
185}
186
187struct ResState {
190 sock_id: u64,
192 status: u16,
194 message: Option<String>,
196 headers: Vec<(String, String)>,
199 body: Vec<u8>,
201}
202
203thread_local! {
204 static CONNS: std::cell::RefCell<HashMap<u64, HttpConn>> =
205 std::cell::RefCell::new(HashMap::new());
206 static RESPONSES: std::cell::RefCell<HashMap<u64, ResState>> =
207 std::cell::RefCell::new(HashMap::new());
208 static NEXT_RESID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
209}
210
211fn next_resid() -> u64 {
212 NEXT_RESID.with(|c| {
213 let id = c.get();
214 c.set(id + 1);
215 id
216 })
217}
218
219pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
223 match method {
224 "createServer" => Some(Ok(create_server(args.first().cloned()))),
225 "request" => Some(request(args, false)),
226 "get" => Some(request(args, true)),
227 "validateHeaderName" => Some(validate_header_name(args)),
228 "validateHeaderValue" => Some(validate_header_value(args)),
229 "setMaxIdleHTTPParsers" | "setGlobalProxyFromEnv" => Some(Ok(Value::Undef)),
231 _ => None,
232 }
233}
234
235fn validate_header_name(args: &[Value]) -> Result<Value, String> {
238 let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
239 if is_valid_token(&name) {
240 Ok(Value::Undef)
241 } else {
242 Err(crate::host::type_error(&format!(
243 "Header name must be a valid HTTP token [\"{name}\"]"
244 )))
245 }
246}
247
248fn validate_header_value(args: &[Value]) -> Result<Value, String> {
252 let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
253 let raw = args.get(1).cloned().unwrap_or(Value::Undef);
254 if matches!(raw, Value::Undef) {
255 return Err(crate::host::type_error(&format!(
256 "Invalid value \"undefined\" for header \"{name}\""
257 )));
258 }
259 let value = with_host(|h| h.str_of(&raw));
260 if value.bytes().any(|b| b != b'\t' && (b < 0x20 || b == 0x7f)) {
261 return Err(crate::host::type_error(&format!(
262 "Invalid character in header content [\"{name}\"]"
263 )));
264 }
265 Ok(Value::Undef)
266}
267
268fn is_valid_token(s: &str) -> bool {
270 !s.is_empty()
271 && s.bytes().all(|b| {
272 b.is_ascii_alphanumeric()
273 || matches!(
274 b,
275 b'!' | b'#'
276 | b'$'
277 | b'%'
278 | b'&'
279 | b'\''
280 | b'*'
281 | b'+'
282 | b'-'
283 | b'.'
284 | b'^'
285 | b'_'
286 | b'`'
287 | b'|'
288 | b'~'
289 )
290 })
291}
292
293pub fn construct_agent(args: &[Value]) -> Value {
297 let pairs = args
298 .first()
299 .filter(|v| matches!(v, Value::Obj(_)))
300 .map(object_pairs)
301 .unwrap_or_default();
302 with_host(|h| {
303 let mut m = IndexMap::new();
304 m.insert("@@native".into(), h.new_str("Agent"));
305 m.insert("maxSockets".into(), Value::Float(f64::INFINITY));
306 m.insert("maxFreeSockets".into(), Value::Float(256.0));
307 m.insert("sockets".into(), h.new_object(IndexMap::new()));
308 m.insert("requests".into(), h.new_object(IndexMap::new()));
309 for (k, v) in pairs {
310 m.insert(k, h.new_str(v));
311 }
312 h.new_object(m)
313 })
314}
315
316pub fn create_server(request_listener: Option<Value>) -> Value {
319 let server = super::net::create_server(None);
320 let listener = request_listener.unwrap_or(Value::Undef);
321 let hook = std::rc::Rc::new(
322 move |_server: &Value, socket: &Value| -> Result<(), String> {
323 let sock_id = socket_id_of(socket);
324 CONNS.with(|c| {
325 c.borrow_mut().insert(
326 sock_id,
327 HttpConn {
328 socket: socket.clone(),
329 listener: listener.clone(),
330 buf: Vec::new(),
331 },
332 );
333 });
334 Ok(())
335 },
336 );
337 super::net::set_conn_hook(&server, hook);
338 server
339}
340
341fn socket_id_of(socket: &Value) -> u64 {
342 with_host(|h| match h.get(socket) {
343 Some(JsObj::Object(p)) => p.get("@@netid").map(|v| h.to_number(v) as u64).unwrap_or(0),
344 _ => 0,
345 })
346}
347
348pub fn drop_conn(sock_id: u64) {
350 CONNS.with(|c| {
351 c.borrow_mut().remove(&sock_id);
352 });
353}
354
355pub fn feed(sock_id: u64, _socket: &Value, bytes: &[u8]) -> Result<(), String> {
360 let is_http = CONNS.with(|c| c.borrow().contains_key(&sock_id));
361 if !is_http {
362 return Ok(());
363 }
364 CONNS.with(|c| {
365 c.borrow_mut()
366 .get_mut(&sock_id)
367 .unwrap()
368 .buf
369 .extend_from_slice(bytes)
370 });
371
372 loop {
374 let (listener, socket, parsed) = CONNS.with(|c| {
375 let mut c = c.borrow_mut();
376 let conn = c.get_mut(&sock_id).unwrap();
377 match parse_request(&conn.buf) {
378 Some((req, consumed)) => {
379 conn.buf.drain(..consumed);
380 (conn.listener.clone(), conn.socket.clone(), Some(req))
381 }
382 None => (Value::Undef, Value::Undef, None),
383 }
384 });
385 let Some(parsed) = parsed else { break };
386
387 let req = build_incoming(&parsed);
388 let res = build_response(sock_id);
389 if with_host(|h| crate::host::is_callable(h, &listener)) {
390 invoke(&listener, vec![req.clone(), res], None)?;
391 }
392 if !parsed.body.is_empty() {
394 let chunk = super::buffer::from_bytes(&parsed.body);
395 super::events::instance_call(
396 &req,
397 "emit",
398 vec![with_host(|h| h.new_str("data")), chunk],
399 )?;
400 }
401 super::events::instance_call(&req, "emit", vec![with_host(|h| h.new_str("end"))])?;
402 let _ = socket; }
404 Ok(())
405}
406
407struct ParsedReq {
409 method: String,
410 url: String,
411 http_version: String,
412 headers: Vec<(String, String)>,
414 body: Vec<u8>,
415}
416
417fn parse_request(buf: &[u8]) -> Option<(ParsedReq, usize)> {
420 let head_end = find_subslice(buf, b"\r\n\r\n")?;
422 let head = &buf[..head_end];
423 let body_start = head_end + 4;
424
425 let head_str = String::from_utf8_lossy(head);
426 let mut lines = head_str.split("\r\n");
427 let request_line = lines.next()?;
428 let mut parts = request_line.split(' ');
429 let method = parts.next()?.to_string();
430 let url = parts.next()?.to_string();
431 let version = parts.next().unwrap_or("HTTP/1.1");
432 let http_version = version.strip_prefix("HTTP/").unwrap_or("1.1").to_string();
433
434 let mut headers: Vec<(String, String)> = Vec::new();
435 let mut content_length = 0usize;
436 for line in lines {
437 if line.is_empty() {
438 continue;
439 }
440 if let Some((k, v)) = line.split_once(':') {
441 let name = k.trim().to_ascii_lowercase();
442 let value = v.trim().to_string();
443 if name == "content-length" {
444 content_length = value.parse().unwrap_or(0);
445 }
446 headers.push((name, value));
447 }
448 }
449
450 if buf.len() < body_start + content_length {
452 return None;
453 }
454 let body = buf[body_start..body_start + content_length].to_vec();
455 Some((
456 ParsedReq {
457 method,
458 url,
459 http_version,
460 headers,
461 body,
462 },
463 body_start + content_length,
464 ))
465}
466
467fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
468 haystack.windows(needle.len()).position(|w| w == needle)
469}
470
471fn build_incoming(req: &ParsedReq) -> Value {
474 let headers_obj = with_host(|h| {
475 let mut m = IndexMap::new();
476 for (k, v) in &req.headers {
477 m.insert(k.clone(), h.new_str(v.clone()));
478 }
479 h.new_object(m)
480 });
481 let mut extra = IndexMap::new();
482 extra.insert(
483 "method".into(),
484 with_host(|h| h.new_str(req.method.clone())),
485 );
486 extra.insert("url".into(), with_host(|h| h.new_str(req.url.clone())));
487 extra.insert(
488 "httpVersion".into(),
489 with_host(|h| h.new_str(req.http_version.clone())),
490 );
491 extra.insert("headers".into(), headers_obj);
492 super::net::new_emitter_object("IncomingMessage", extra)
493}
494
495fn build_response(sock_id: u64) -> Value {
498 let resid = next_resid();
499 RESPONSES.with(|r| {
500 r.borrow_mut().insert(
501 resid,
502 ResState {
503 sock_id,
504 status: 200,
505 message: None,
506 headers: Vec::new(),
507 body: Vec::new(),
508 },
509 );
510 });
511 let mut extra = IndexMap::new();
512 extra.insert("@@resid".into(), Value::Float(resid as f64));
513 extra.insert("statusCode".into(), Value::Float(200.0));
514 super::net::new_emitter_object("ServerResponse", extra)
515}
516
517fn resid_of(res: &Value) -> Option<u64> {
518 with_host(|h| match h.get(res) {
519 Some(JsObj::Object(p)) => p.get("@@resid").map(|v| h.to_number(v) as u64),
520 _ => None,
521 })
522}
523
524pub fn instance_call(
527 tag: &str,
528 recv: &Value,
529 method: &str,
530 args: Vec<Value>,
531) -> Result<Value, String> {
532 if matches!(
534 method,
535 "on" | "addListener"
536 | "prependListener"
537 | "once"
538 | "prependOnceListener"
539 | "emit"
540 | "removeListener"
541 | "off"
542 | "removeAllListeners"
543 | "listenerCount"
544 | "eventNames"
545 ) {
546 return super::events::instance_call(recv, method, args);
547 }
548 match tag {
549 "IncomingMessage" => Err(crate::host::type_error(&format!(
550 "req.{method} is not a function"
551 ))),
552 "ServerResponse" => response_call(recv, method, args),
553 "ClientRequest" => client_request_call(recv, method, args),
554 "Agent" => match method {
556 "destroy" => Ok(Value::Undef),
557 "getName" => Ok(with_host(|h| h.new_str("localhost:80:"))),
558 _ => Err(crate::host::type_error(&format!(
559 "agent.{method} is not a function"
560 ))),
561 },
562 _ => Err(crate::host::type_error(&format!(
563 "{method} is not a function"
564 ))),
565 }
566}
567
568fn response_call(res: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
569 let Some(resid) = resid_of(res) else {
570 return Err(crate::host::type_error("invalid ServerResponse"));
571 };
572 match method {
573 "writeHead" => {
574 let status =
575 with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u16;
576 let mut message: Option<String> = None;
578 let mut headers_arg: Option<Value> = None;
579 if let Some(a) = args.get(1) {
580 if with_host(|h| h.as_str(a)).is_some() {
581 message = Some(with_host(|h| h.str_of(a)));
582 } else if !matches!(a, Value::Undef) {
583 headers_arg = Some(a.clone());
584 }
585 }
586 if let Some(a) = args.get(2) {
587 if !matches!(a, Value::Undef) {
588 headers_arg = Some(a.clone());
589 }
590 }
591 let header_pairs = headers_arg.map(|h| object_pairs(&h)).unwrap_or_default();
592 RESPONSES.with(|r| {
593 if let Some(st) = r.borrow_mut().get_mut(&resid) {
594 st.status = status;
595 st.message = message;
596 for (k, v) in header_pairs {
597 upsert_header(&mut st.headers, &k, v);
598 }
599 }
600 });
601 set_res_prop(res, "statusCode", Value::Float(status as f64));
603 Ok(res.clone())
604 }
605 "setHeader" => {
606 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
607 let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
608 RESPONSES.with(|r| {
609 if let Some(st) = r.borrow_mut().get_mut(&resid) {
610 upsert_header(&mut st.headers, &k, v);
611 }
612 });
613 Ok(Value::Undef)
614 }
615 "getHeader" => {
616 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
617 .to_ascii_lowercase();
618 let val = RESPONSES.with(|r| {
619 r.borrow().get(&resid).and_then(|st| {
620 st.headers
621 .iter()
622 .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
623 .map(|(_, v)| v.clone())
624 })
625 });
626 Ok(val
627 .map(|v| with_host(|h| h.new_str(v)))
628 .unwrap_or(Value::Undef))
629 }
630 "removeHeader" => {
631 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
632 RESPONSES.with(|r| {
633 if let Some(st) = r.borrow_mut().get_mut(&resid) {
634 st.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
635 }
636 });
637 Ok(Value::Undef)
638 }
639 "write" => {
640 let bytes = value_bytes(args.first());
641 RESPONSES.with(|r| {
642 if let Some(st) = r.borrow_mut().get_mut(&resid) {
643 st.body.extend_from_slice(&bytes);
644 }
645 });
646 Ok(Value::Bool(true))
647 }
648 "end" => {
649 if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
650 let bytes = value_bytes(Some(chunk));
651 RESPONSES.with(|r| {
652 if let Some(st) = r.borrow_mut().get_mut(&resid) {
653 st.body.extend_from_slice(&bytes);
654 }
655 });
656 }
657 finish_response(res, resid)?;
658 Ok(res.clone())
659 }
660 _ => Err(crate::host::type_error(&format!(
661 "res.{method} is not a function"
662 ))),
663 }
664}
665
666fn finish_response(res: &Value, resid: u64) -> Result<(), String> {
668 let js_status = with_host(|h| match h.get(res) {
670 Some(JsObj::Object(p)) => p.get("statusCode").map(|v| h.to_number(v) as u16),
671 _ => None,
672 });
673 let st = RESPONSES.with(|r| r.borrow_mut().remove(&resid));
674 let Some(mut st) = st else { return Ok(()) };
675 if let Some(s) = js_status {
676 st.status = s;
677 }
678 let payload = serialize_response(&mut st);
679 super::net::socket_write_id(st.sock_id, &payload);
680 super::events::instance_call(res, "emit", vec![with_host(|h| h.new_str("finish"))])?;
681 Ok(())
682}
683
684fn serialize_response(st: &mut ResState) -> Vec<u8> {
688 let reason = st
689 .message
690 .clone()
691 .unwrap_or_else(|| status_text(st.status).to_string());
692 let mut out = format!("HTTP/1.1 {} {}\r\n", st.status, reason).into_bytes();
693
694 let has = |name: &str| st.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
695 let chunked = st.headers.iter().any(|(k, v)| {
696 k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
697 });
698
699 for (k, v) in &st.headers {
700 out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
701 }
702 if !chunked && !has("content-length") {
703 out.extend_from_slice(format!("Content-Length: {}\r\n", st.body.len()).as_bytes());
704 }
705 if !has("connection") {
706 out.extend_from_slice(b"Connection: keep-alive\r\n");
707 }
708 out.extend_from_slice(b"\r\n");
709 out.extend_from_slice(&st.body);
710 out
711}
712
713fn upsert_header(headers: &mut Vec<(String, String)>, name: &str, value: String) {
717 if let Some(slot) = headers
718 .iter_mut()
719 .find(|(k, _)| k.eq_ignore_ascii_case(name))
720 {
721 slot.1 = value;
722 } else {
723 headers.push((name.to_string(), value));
724 }
725}
726
727fn object_pairs(obj: &Value) -> Vec<(String, String)> {
730 with_host(|h| match h.get(obj) {
731 Some(JsObj::Object(p)) => p
732 .iter()
733 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
734 .map(|(k, v)| (k.clone(), h.str_of(v)))
735 .collect(),
736 _ => Vec::new(),
737 })
738}
739
740fn set_res_prop(res: &Value, key: &str, val: Value) {
742 with_host(|h| {
743 if let Some(JsObj::Object(p)) = h.get_mut(res) {
744 p.insert(key.to_string(), val);
745 }
746 });
747}
748
749fn value_bytes(v: Option<&Value>) -> Vec<u8> {
751 let Some(v) = v else { return Vec::new() };
752 let is_buffer =
753 with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
754 if is_buffer {
755 return with_host(|h| match h.get(v) {
756 Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
757 Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
758 _ => Vec::new(),
759 },
760 _ => Vec::new(),
761 });
762 }
763 with_host(|h| h.str_of(v)).into_bytes()
764}
765
766struct ClientReq {
776 host: String,
777 port: u16,
778 method: String,
779 path: String,
780 headers: Vec<(String, String)>,
781 body: Vec<u8>,
782 request: Value,
784 sent: bool,
785}
786
787thread_local! {
788 static CLIENT_REQS: std::cell::RefCell<HashMap<u64, ClientReq>> =
789 std::cell::RefCell::new(HashMap::new());
790 static NEXT_REQID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
791}
792
793fn next_reqid() -> u64 {
794 NEXT_REQID.with(|c| {
795 let id = c.get();
796 c.set(id + 1);
797 id
798 })
799}
800
801fn get_prop(recv: &Value, key: &str) -> Option<Value> {
802 with_host(|h| match h.get(recv) {
803 Some(JsObj::Object(p)) => p.get(key).cloned(),
804 _ => None,
805 })
806}
807
808fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
809 get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
810}
811
812pub fn request(args: &[Value], is_get: bool) -> Result<Value, String> {
815 let mut host = "localhost".to_string();
816 let mut port: u16 = 80;
817 let mut path = "/".to_string();
818 let mut method = "GET".to_string();
819 let mut headers: Vec<(String, String)> = Vec::new();
820 let mut cb: Option<Value> = None;
821
822 for a in args {
823 if with_host(|h| crate::host::is_callable(h, a)) {
824 cb = Some(a.clone());
825 } else if with_host(|h| h.as_str(a)).is_some() {
826 let url = with_host(|h| h.str_of(a));
827 parse_url(&url, &mut host, &mut port, &mut path);
828 } else if matches!(a, Value::Obj(_)) {
829 for key in ["hostname", "host"] {
830 if let Some(v) = get_prop(a, key).filter(|v| with_host(|h| h.as_str(v)).is_some()) {
831 host = with_host(|h| h.str_of(&v));
832 }
833 }
834 if let Some(v) = get_prop(a, "port") {
835 let n = with_host(|h| h.to_number(&v));
836 if !n.is_nan() {
837 port = n as u16;
838 }
839 }
840 if let Some(v) = get_prop(a, "path").filter(|v| with_host(|h| h.as_str(v)).is_some()) {
841 path = with_host(|h| h.str_of(&v));
842 }
843 if let Some(v) = get_prop(a, "method").filter(|v| with_host(|h| h.as_str(v)).is_some())
844 {
845 method = with_host(|h| h.str_of(&v));
846 }
847 if let Some(hv) = get_prop(a, "headers").filter(|v| matches!(v, Value::Obj(_))) {
848 for (k, val) in object_pairs(&hv) {
849 headers.push((k, val));
850 }
851 }
852 }
853 }
854 if is_get {
855 method = "GET".to_string();
856 }
857
858 let reqid = next_reqid();
859 let mut extra = IndexMap::new();
860 extra.insert("@@reqid".into(), Value::Float(reqid as f64));
861 extra.insert("method".into(), with_host(|h| h.new_str(method.clone())));
862 extra.insert("path".into(), with_host(|h| h.new_str(path.clone())));
863 let request = super::net::new_emitter_object("ClientRequest", extra);
864 if let Some(cb) = cb {
866 super::events::instance_call(
867 &request,
868 "on",
869 vec![with_host(|h| h.new_str("response")), cb],
870 )?;
871 }
872 CLIENT_REQS.with(|c| {
873 c.borrow_mut().insert(
874 reqid,
875 ClientReq {
876 host,
877 port,
878 method,
879 path,
880 headers,
881 body: Vec::new(),
882 request: request.clone(),
883 sent: false,
884 },
885 );
886 });
887 if is_get {
888 dispatch_request(reqid)?;
889 }
890 Ok(request)
891}
892
893fn parse_url(url: &str, host: &mut String, port: &mut u16, path: &mut String) {
895 let rest = url.strip_prefix("http://").unwrap_or(url);
896 let (authority, p) = match rest.find('/') {
897 Some(i) => (&rest[..i], &rest[i..]),
898 None => (rest, "/"),
899 };
900 *path = if p.is_empty() {
901 "/".to_string()
902 } else {
903 p.to_string()
904 };
905 if let Some((h, port_str)) = authority.rsplit_once(':') {
906 *host = h.to_string();
907 if let Ok(n) = port_str.parse::<u16>() {
908 *port = n;
909 }
910 } else {
911 *host = authority.to_string();
912 *port = 80;
913 }
914}
915
916fn client_request_call(req: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
917 let reqid = u64_prop(req, "@@reqid");
918 match method {
919 "write" => {
920 if let Some(id) = reqid {
921 let bytes = value_bytes(args.first());
922 CLIENT_REQS.with(|c| {
923 if let Some(r) = c.borrow_mut().get_mut(&id) {
924 r.body.extend_from_slice(&bytes);
925 }
926 });
927 }
928 Ok(Value::Bool(true))
929 }
930 "end" => {
931 if let Some(id) = reqid {
932 if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
933 let bytes = value_bytes(Some(chunk));
934 CLIENT_REQS.with(|c| {
935 if let Some(r) = c.borrow_mut().get_mut(&id) {
936 r.body.extend_from_slice(&bytes);
937 }
938 });
939 }
940 dispatch_request(id)?;
941 }
942 Ok(req.clone())
943 }
944 "setHeader" => {
945 if let Some(id) = reqid {
946 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
947 let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
948 CLIENT_REQS.with(|c| {
949 if let Some(r) = c.borrow_mut().get_mut(&id) {
950 upsert_header(&mut r.headers, &k, v);
951 }
952 });
953 }
954 Ok(Value::Undef)
955 }
956 "getHeader" => {
957 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
958 .to_ascii_lowercase();
959 let val = reqid.and_then(|id| {
960 CLIENT_REQS.with(|c| {
961 c.borrow().get(&id).and_then(|r| {
962 r.headers
963 .iter()
964 .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
965 .map(|(_, v)| v.clone())
966 })
967 })
968 });
969 Ok(val
970 .map(|v| with_host(|h| h.new_str(v)))
971 .unwrap_or(Value::Undef))
972 }
973 "removeHeader" => {
974 if let Some(id) = reqid {
975 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
976 CLIENT_REQS.with(|c| {
977 if let Some(r) = c.borrow_mut().get_mut(&id) {
978 r.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
979 }
980 });
981 }
982 Ok(Value::Undef)
983 }
984 "abort" | "destroy" | "setTimeout" | "flushHeaders" => Ok(req.clone()),
985 _ => Err(crate::host::type_error(&format!(
986 "req.{method} is not a function"
987 ))),
988 }
989}
990
991fn dispatch_request(reqid: u64) -> Result<(), String> {
994 let sent = CLIENT_REQS.with(|c| c.borrow().get(&reqid).map(|r| r.sent).unwrap_or(true));
995 if sent {
996 return Ok(());
997 }
998 CLIENT_REQS.with(|c| {
999 if let Some(r) = c.borrow_mut().get_mut(&reqid) {
1000 r.sent = true;
1001 }
1002 });
1003
1004 let (host, port, method, path, headers, body) = CLIENT_REQS.with(|c| {
1005 let b = c.borrow();
1006 let r = b.get(&reqid).unwrap();
1007 (
1008 r.host.clone(),
1009 r.port,
1010 r.method.clone(),
1011 r.path.clone(),
1012 r.headers.clone(),
1013 r.body.clone(),
1014 )
1015 });
1016
1017 let io_tx = with_host(|h| h.io_sender());
1018 with_host(|h| h.incr_handle());
1019
1020 let mut has_host = false;
1022 let mut has_len = false;
1023 let mut header_block = String::new();
1024 for (k, v) in &headers {
1025 if k.eq_ignore_ascii_case("host") {
1026 has_host = true;
1027 }
1028 if k.eq_ignore_ascii_case("content-length") {
1029 has_len = true;
1030 }
1031 if k.eq_ignore_ascii_case("connection") {
1032 continue;
1033 }
1034 header_block.push_str(&format!("{k}: {v}\r\n"));
1035 }
1036 let host_header = if port == 80 {
1037 host.clone()
1038 } else {
1039 format!("{host}:{port}")
1040 };
1041 let mut request_bytes = format!("{method} {path} HTTP/1.1\r\n");
1042 if !has_host {
1043 request_bytes.push_str(&format!("Host: {host_header}\r\n"));
1044 }
1045 request_bytes.push_str(&header_block);
1046 if !has_len && !body.is_empty() {
1047 request_bytes.push_str(&format!("Content-Length: {}\r\n", body.len()));
1048 }
1049 request_bytes.push_str("Connection: close\r\n\r\n");
1050 let mut wire = request_bytes.into_bytes();
1051 wire.extend_from_slice(&body);
1052
1053 std::thread::spawn(move || match do_client_exchange(&host, port, &wire) {
1054 Ok(raw) => {
1055 let _ = io_tx.send(Box::new(move || deliver_response(reqid, raw)));
1056 }
1057 Err(msg) => {
1058 let _ = io_tx.send(Box::new(move || deliver_error(reqid, msg)));
1059 }
1060 });
1061 Ok(())
1062}
1063
1064fn do_client_exchange(host: &str, port: u16, request: &[u8]) -> Result<Vec<u8>, String> {
1066 let mut stream = TcpStream::connect((host, port))
1067 .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
1068 stream
1069 .write_all(request)
1070 .map_err(|e| format!("Error: http write: {e}"))?;
1071 stream
1072 .flush()
1073 .map_err(|e| format!("Error: http flush: {e}"))?;
1074 let mut raw = Vec::new();
1075 let mut buf = [0u8; 16384];
1076 loop {
1077 match stream.read(&mut buf) {
1078 Ok(0) => break,
1079 Ok(n) => raw.extend_from_slice(&buf[..n]),
1080 Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1081 Err(e) => {
1082 if raw.is_empty() {
1083 return Err(format!("Error: http read: {e}"));
1084 }
1085 break;
1086 }
1087 }
1088 }
1089 Ok(raw)
1090}
1091
1092fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
1095 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1096 with_host(|h| h.decr_handle());
1097 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1098 let Some(entry) = entry else { return Ok(()) };
1099
1100 let (status, message, http_version, headers, body) = parse_response(&raw);
1101
1102 let headers_obj = with_host(|h| {
1103 let mut m = IndexMap::new();
1104 for (k, v) in &headers {
1105 m.insert(k.clone(), h.new_str(v.clone()));
1106 }
1107 h.new_object(m)
1108 });
1109 let mut extra = IndexMap::new();
1110 extra.insert("statusCode".into(), Value::Float(status as f64));
1111 extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
1112 extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
1113 extra.insert("headers".into(), headers_obj);
1114 let res = super::net::new_emitter_object("IncomingMessage", extra);
1115
1116 super::events::instance_call(
1117 &entry.request,
1118 "emit",
1119 vec![with_host(|h| h.new_str("response")), res.clone()],
1120 )?;
1121 if !body.is_empty() {
1122 let chunk = super::buffer::from_bytes(&body);
1123 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
1124 }
1125 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
1126 Ok(())
1127}
1128
1129fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
1131 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1132 with_host(|h| h.decr_handle());
1133 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1134 if let Some(entry) = entry {
1135 let err = with_host(|h| {
1136 let mut m = IndexMap::new();
1137 m.insert("message".into(), h.new_str(msg.clone()));
1138 h.new_object(m)
1139 });
1140 super::events::instance_call(
1141 &entry.request,
1142 "emit",
1143 vec![with_host(|h| h.new_str("error")), err],
1144 )?;
1145 }
1146 Ok(())
1147}
1148
1149fn parse_response(raw: &[u8]) -> (u16, String, String, Vec<(String, String)>, Vec<u8>) {
1152 let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
1153 let head = String::from_utf8_lossy(&raw[..head_end]);
1154 let body_start = (head_end + 4).min(raw.len());
1155 let mut lines = head.split("\r\n");
1156 let status_line = lines.next().unwrap_or("");
1157 let mut sp = status_line.splitn(3, ' ');
1158 let version = sp
1159 .next()
1160 .unwrap_or("HTTP/1.1")
1161 .strip_prefix("HTTP/")
1162 .unwrap_or("1.1")
1163 .to_string();
1164 let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
1165 let message = sp.next().unwrap_or("").to_string();
1166
1167 let mut headers: Vec<(String, String)> = Vec::new();
1168 let mut chunked = false;
1169 for line in lines {
1170 if line.is_empty() {
1171 continue;
1172 }
1173 if let Some((k, v)) = line.split_once(':') {
1174 let name = k.trim().to_ascii_lowercase();
1175 let value = v.trim().to_string();
1176 if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1177 chunked = true;
1178 }
1179 headers.push((name, value));
1180 }
1181 }
1182 let raw_body = &raw[body_start..];
1183 let body = if chunked {
1184 decode_chunked(raw_body)
1185 } else {
1186 raw_body.to_vec()
1187 };
1188 (status, message, version, headers, body)
1189}
1190
1191fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1194 let mut out = Vec::new();
1195 while let Some(nl) = find_subslice(data, b"\r\n") {
1196 let size_line = String::from_utf8_lossy(&data[..nl]);
1197 let size_hex = size_line.split(';').next().unwrap_or("").trim();
1198 let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1199 if size == 0 {
1200 break;
1201 }
1202 let chunk_start = nl + 2;
1203 let chunk_end = (chunk_start + size).min(data.len());
1204 out.extend_from_slice(&data[chunk_start..chunk_end]);
1205 let next = chunk_end + 2;
1206 if next >= data.len() {
1207 break;
1208 }
1209 data = &data[next..];
1210 }
1211 out
1212}
1213
1214fn status_text(code: u16) -> &'static str {
1216 match code {
1217 200 => "OK",
1218 201 => "Created",
1219 202 => "Accepted",
1220 204 => "No Content",
1221 301 => "Moved Permanently",
1222 302 => "Found",
1223 304 => "Not Modified",
1224 400 => "Bad Request",
1225 401 => "Unauthorized",
1226 403 => "Forbidden",
1227 404 => "Not Found",
1228 405 => "Method Not Allowed",
1229 409 => "Conflict",
1230 500 => "Internal Server Error",
1231 502 => "Bad Gateway",
1232 503 => "Service Unavailable",
1233 _ => "OK",
1234 }
1235}