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::coded_error(
243 "TypeError",
244 "ERR_INVALID_HTTP_TOKEN",
245 &format!("Header name must be a valid HTTP token [\"{name}\"]"),
246 ))
247 }
248}
249
250fn validate_header_value(args: &[Value]) -> Result<Value, String> {
254 let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
255 let raw = args.get(1).cloned().unwrap_or(Value::Undef);
256 if matches!(raw, Value::Undef) {
257 return Err(crate::host::coded_error(
258 "TypeError",
259 "ERR_HTTP_INVALID_HEADER_VALUE",
260 &format!("Invalid value \"undefined\" for header \"{name}\""),
261 ));
262 }
263 let value = with_host(|h| h.str_of(&raw));
264 if value.bytes().any(|b| b != b'\t' && (b < 0x20 || b == 0x7f)) {
265 return Err(crate::host::coded_error(
266 "TypeError",
267 "ERR_INVALID_CHAR",
268 &format!("Invalid character in header content [\"{name}\"]"),
269 ));
270 }
271 Ok(Value::Undef)
272}
273
274fn is_valid_token(s: &str) -> bool {
276 !s.is_empty()
277 && s.bytes().all(|b| {
278 b.is_ascii_alphanumeric()
279 || matches!(
280 b,
281 b'!' | b'#'
282 | b'$'
283 | b'%'
284 | b'&'
285 | b'\''
286 | b'*'
287 | b'+'
288 | b'-'
289 | b'.'
290 | b'^'
291 | b'_'
292 | b'`'
293 | b'|'
294 | b'~'
295 )
296 })
297}
298
299pub fn construct_agent(args: &[Value]) -> Value {
303 let pairs = args
304 .first()
305 .filter(|v| matches!(v, Value::Obj(_)))
306 .map(object_pairs)
307 .unwrap_or_default();
308 with_host(|h| {
309 let mut m = IndexMap::new();
310 m.insert("@@native".into(), h.new_str("Agent"));
311 m.insert("maxSockets".into(), Value::Float(f64::INFINITY));
312 m.insert("maxFreeSockets".into(), Value::Float(256.0));
313 m.insert("sockets".into(), h.new_object(IndexMap::new()));
314 m.insert("requests".into(), h.new_object(IndexMap::new()));
315 for (k, v) in pairs {
316 m.insert(k, h.new_str(v));
317 }
318 h.new_object(m)
319 })
320}
321
322pub fn create_server(request_listener: Option<Value>) -> Value {
325 let server = super::net::create_server(None);
326 let listener = request_listener.unwrap_or(Value::Undef);
327 let hook = std::rc::Rc::new(
328 move |_server: &Value, socket: &Value| -> Result<(), String> {
329 let sock_id = socket_id_of(socket);
330 CONNS.with(|c| {
331 c.borrow_mut().insert(
332 sock_id,
333 HttpConn {
334 socket: socket.clone(),
335 listener: listener.clone(),
336 buf: Vec::new(),
337 },
338 );
339 });
340 Ok(())
341 },
342 );
343 super::net::set_conn_hook(&server, hook);
344 server
345}
346
347fn socket_id_of(socket: &Value) -> u64 {
348 with_host(|h| match h.get(socket) {
349 Some(JsObj::Object(p)) => p.get("@@netid").map(|v| h.to_number(v) as u64).unwrap_or(0),
350 _ => 0,
351 })
352}
353
354pub fn drop_conn(sock_id: u64) {
356 CONNS.with(|c| {
357 c.borrow_mut().remove(&sock_id);
358 });
359}
360
361pub fn feed(sock_id: u64, _socket: &Value, bytes: &[u8]) -> Result<(), String> {
366 let is_http = CONNS.with(|c| c.borrow().contains_key(&sock_id));
367 if !is_http {
368 return Ok(());
369 }
370 CONNS.with(|c| {
371 c.borrow_mut()
372 .get_mut(&sock_id)
373 .unwrap()
374 .buf
375 .extend_from_slice(bytes)
376 });
377
378 loop {
380 let (listener, socket, parsed) = CONNS.with(|c| {
381 let mut c = c.borrow_mut();
382 let conn = c.get_mut(&sock_id).unwrap();
383 match parse_request(&conn.buf) {
384 Some((req, consumed)) => {
385 conn.buf.drain(..consumed);
386 (conn.listener.clone(), conn.socket.clone(), Some(req))
387 }
388 None => (Value::Undef, Value::Undef, None),
389 }
390 });
391 let Some(parsed) = parsed else { break };
392
393 let req = build_incoming(&parsed);
394 let res = build_response(sock_id);
395 if with_host(|h| crate::host::is_callable(h, &listener)) {
396 invoke(&listener, vec![req.clone(), res], None)?;
397 }
398 if !parsed.body.is_empty() {
402 let encoding = with_host(|h| match h.get(&req) {
403 Some(JsObj::Object(p)) => p.get("@@encoding").map(|v| h.str_of(v)),
404 _ => None,
405 });
406 let chunk = match encoding {
407 Some(enc) => with_host(|h| {
408 let s = super::buffer::encode_bytes(&parsed.body, &enc);
409 h.new_str(s)
410 }),
411 None => super::buffer::from_bytes(&parsed.body),
412 };
413 super::events::instance_call(
414 &req,
415 "emit",
416 vec![with_host(|h| h.new_str("data")), chunk],
417 )?;
418 }
419 super::events::instance_call(&req, "emit", vec![with_host(|h| h.new_str("end"))])?;
420 let _ = socket; }
422 Ok(())
423}
424
425struct ParsedReq {
427 method: String,
428 url: String,
429 http_version: String,
430 headers: Vec<(String, String)>,
432 body: Vec<u8>,
433}
434
435fn parse_request(buf: &[u8]) -> Option<(ParsedReq, usize)> {
438 let head_end = find_subslice(buf, b"\r\n\r\n")?;
440 let head = &buf[..head_end];
441 let body_start = head_end + 4;
442
443 let head_str = String::from_utf8_lossy(head);
444 let mut lines = head_str.split("\r\n");
445 let request_line = lines.next()?;
446 let mut parts = request_line.split(' ');
447 let method = parts.next()?.to_string();
448 let url = parts.next()?.to_string();
449 let version = parts.next().unwrap_or("HTTP/1.1");
450 let http_version = version.strip_prefix("HTTP/").unwrap_or("1.1").to_string();
451
452 let mut headers: Vec<(String, String)> = Vec::new();
453 let mut content_length = 0usize;
454 for line in lines {
455 if line.is_empty() {
456 continue;
457 }
458 if let Some((k, v)) = line.split_once(':') {
459 let name = k.trim().to_ascii_lowercase();
460 let value = v.trim().to_string();
461 if name == "content-length" {
462 content_length = value.parse().unwrap_or(0);
463 }
464 headers.push((name, value));
465 }
466 }
467
468 if buf.len() < body_start + content_length {
470 return None;
471 }
472 let body = buf[body_start..body_start + content_length].to_vec();
473 Some((
474 ParsedReq {
475 method,
476 url,
477 http_version,
478 headers,
479 body,
480 },
481 body_start + content_length,
482 ))
483}
484
485fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
486 haystack.windows(needle.len()).position(|w| w == needle)
487}
488
489fn build_incoming(req: &ParsedReq) -> Value {
492 let headers_obj = with_host(|h| {
493 let mut m = IndexMap::new();
494 for (k, v) in &req.headers {
495 m.insert(k.clone(), h.new_str(v.clone()));
496 }
497 h.new_object(m)
498 });
499 let mut extra = IndexMap::new();
500 extra.insert(
501 "method".into(),
502 with_host(|h| h.new_str(req.method.clone())),
503 );
504 extra.insert("url".into(), with_host(|h| h.new_str(req.url.clone())));
505 extra.insert(
506 "httpVersion".into(),
507 with_host(|h| h.new_str(req.http_version.clone())),
508 );
509 extra.insert("headers".into(), headers_obj);
510 super::net::new_emitter_object("IncomingMessage", extra)
511}
512
513fn incoming_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
520 match method {
521 "pause" | "resume" | "unpipe" | "destroy" => Ok(recv.clone()),
525 "isPaused" => Ok(Value::Bool(false)),
526 "setEncoding" => {
528 let enc = super::arg_str(&args, 0);
529 with_host(|h| {
530 let v = h.new_str(enc);
531 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
532 p.insert("@@encoding".into(), v);
533 }
534 });
535 Ok(recv.clone())
536 }
537 _ => Err(crate::host::type_error(&format!(
538 "req.{method} is not a function"
539 ))),
540 }
541}
542
543fn build_response(sock_id: u64) -> Value {
546 let resid = next_resid();
547 RESPONSES.with(|r| {
548 r.borrow_mut().insert(
549 resid,
550 ResState {
551 sock_id,
552 status: 200,
553 message: None,
554 headers: Vec::new(),
555 body: Vec::new(),
556 },
557 );
558 });
559 let mut extra = IndexMap::new();
560 extra.insert("@@resid".into(), Value::Float(resid as f64));
561 extra.insert("statusCode".into(), Value::Float(200.0));
562 super::net::new_emitter_object("ServerResponse", extra)
563}
564
565fn resid_of(res: &Value) -> Option<u64> {
566 with_host(|h| match h.get(res) {
567 Some(JsObj::Object(p)) => p.get("@@resid").map(|v| h.to_number(v) as u64),
568 _ => None,
569 })
570}
571
572pub fn instance_call(
575 tag: &str,
576 recv: &Value,
577 method: &str,
578 args: Vec<Value>,
579) -> Result<Value, String> {
580 if super::events::METHODS.contains(&method) {
586 return super::events::instance_call(recv, method, args);
587 }
588 match tag {
589 "IncomingMessage" => incoming_call(recv, method, args),
590 "ServerResponse" => response_call(recv, method, args),
591 "ClientRequest" => client_request_call(recv, method, args),
592 "Agent" => match method {
594 "destroy" => Ok(Value::Undef),
595 "getName" => Ok(with_host(|h| h.new_str("localhost:80:"))),
596 _ => Err(crate::host::type_error(&format!(
597 "agent.{method} is not a function"
598 ))),
599 },
600 _ => Err(crate::host::type_error(&format!(
601 "{method} is not a function"
602 ))),
603 }
604}
605
606fn response_call(res: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
607 let Some(resid) = resid_of(res) else {
608 return Err(crate::host::type_error("invalid ServerResponse"));
609 };
610 match method {
611 "writeHead" => {
612 let status =
613 with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u16;
614 let mut message: Option<String> = None;
616 let mut headers_arg: Option<Value> = None;
617 if let Some(a) = args.get(1) {
618 if with_host(|h| h.as_str(a)).is_some() {
619 message = Some(with_host(|h| h.str_of(a)));
620 } else if !matches!(a, Value::Undef) {
621 headers_arg = Some(a.clone());
622 }
623 }
624 if let Some(a) = args.get(2) {
625 if !matches!(a, Value::Undef) {
626 headers_arg = Some(a.clone());
627 }
628 }
629 let header_pairs = headers_arg.map(|h| object_pairs(&h)).unwrap_or_default();
630 RESPONSES.with(|r| {
631 if let Some(st) = r.borrow_mut().get_mut(&resid) {
632 st.status = status;
633 st.message = message;
634 for (k, v) in header_pairs {
635 upsert_header(&mut st.headers, &k, v);
636 }
637 }
638 });
639 set_res_prop(res, "statusCode", Value::Float(status as f64));
641 Ok(res.clone())
642 }
643 "setHeader" => {
644 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
645 let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
646 RESPONSES.with(|r| {
647 if let Some(st) = r.borrow_mut().get_mut(&resid) {
648 upsert_header(&mut st.headers, &k, v);
649 }
650 });
651 Ok(Value::Undef)
652 }
653 "getHeader" => {
654 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
655 .to_ascii_lowercase();
656 let val = RESPONSES.with(|r| {
657 r.borrow().get(&resid).and_then(|st| {
658 st.headers
659 .iter()
660 .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
661 .map(|(_, v)| v.clone())
662 })
663 });
664 Ok(val
665 .map(|v| with_host(|h| h.new_str(v)))
666 .unwrap_or(Value::Undef))
667 }
668 "removeHeader" => {
669 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
670 RESPONSES.with(|r| {
671 if let Some(st) = r.borrow_mut().get_mut(&resid) {
672 st.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
673 }
674 });
675 Ok(Value::Undef)
676 }
677 "write" => {
678 let bytes = value_bytes(args.first());
679 RESPONSES.with(|r| {
680 if let Some(st) = r.borrow_mut().get_mut(&resid) {
681 st.body.extend_from_slice(&bytes);
682 }
683 });
684 Ok(Value::Bool(true))
685 }
686 "end" => {
687 if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
688 let bytes = value_bytes(Some(chunk));
689 RESPONSES.with(|r| {
690 if let Some(st) = r.borrow_mut().get_mut(&resid) {
691 st.body.extend_from_slice(&bytes);
692 }
693 });
694 }
695 finish_response(res, resid)?;
696 Ok(res.clone())
697 }
698 _ => Err(crate::host::type_error(&format!(
699 "res.{method} is not a function"
700 ))),
701 }
702}
703
704fn finish_response(res: &Value, resid: u64) -> Result<(), String> {
706 let js_status = with_host(|h| match h.get(res) {
708 Some(JsObj::Object(p)) => p.get("statusCode").map(|v| h.to_number(v) as u16),
709 _ => None,
710 });
711 let st = RESPONSES.with(|r| r.borrow_mut().remove(&resid));
712 let Some(mut st) = st else { return Ok(()) };
713 if let Some(s) = js_status {
714 st.status = s;
715 }
716 let payload = serialize_response(&mut st);
717 super::net::socket_write_id(st.sock_id, &payload);
718 super::events::instance_call(res, "emit", vec![with_host(|h| h.new_str("finish"))])?;
719 Ok(())
720}
721
722fn serialize_response(st: &mut ResState) -> Vec<u8> {
726 let reason = st
727 .message
728 .clone()
729 .unwrap_or_else(|| status_text(st.status).to_string());
730 let mut out = format!("HTTP/1.1 {} {}\r\n", st.status, reason).into_bytes();
731
732 let has = |name: &str| st.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
733 let chunked = st.headers.iter().any(|(k, v)| {
734 k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
735 });
736
737 for (k, v) in &st.headers {
738 out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
739 }
740 if !chunked && !has("content-length") {
741 out.extend_from_slice(format!("Content-Length: {}\r\n", st.body.len()).as_bytes());
742 }
743 if !has("connection") {
744 out.extend_from_slice(b"Connection: keep-alive\r\n");
745 }
746 out.extend_from_slice(b"\r\n");
747 out.extend_from_slice(&st.body);
748 out
749}
750
751fn upsert_header(headers: &mut Vec<(String, String)>, name: &str, value: String) {
755 if let Some(slot) = headers
756 .iter_mut()
757 .find(|(k, _)| k.eq_ignore_ascii_case(name))
758 {
759 slot.1 = value;
760 } else {
761 headers.push((name.to_string(), value));
762 }
763}
764
765fn object_pairs(obj: &Value) -> Vec<(String, String)> {
768 with_host(|h| match h.get(obj) {
769 Some(JsObj::Object(p)) => p
770 .iter()
771 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
772 .map(|(k, v)| (k.clone(), h.str_of(v)))
773 .collect(),
774 _ => Vec::new(),
775 })
776}
777
778fn set_res_prop(res: &Value, key: &str, val: Value) {
780 with_host(|h| {
781 if let Some(JsObj::Object(p)) = h.get_mut(res) {
782 p.insert(key.to_string(), val);
783 }
784 });
785}
786
787fn value_bytes(v: Option<&Value>) -> Vec<u8> {
789 let Some(v) = v else { return Vec::new() };
790 let is_buffer =
791 with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
792 if is_buffer {
793 return with_host(|h| match h.get(v) {
794 Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
795 Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
796 _ => Vec::new(),
797 },
798 _ => Vec::new(),
799 });
800 }
801 with_host(|h| h.str_of(v)).into_bytes()
802}
803
804struct ClientReq {
814 host: String,
815 port: u16,
816 method: String,
817 path: String,
818 headers: Vec<(String, String)>,
819 body: Vec<u8>,
820 request: Value,
822 sent: bool,
823}
824
825thread_local! {
826 static CLIENT_REQS: std::cell::RefCell<HashMap<u64, ClientReq>> =
827 std::cell::RefCell::new(HashMap::new());
828 static NEXT_REQID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
829}
830
831fn next_reqid() -> u64 {
832 NEXT_REQID.with(|c| {
833 let id = c.get();
834 c.set(id + 1);
835 id
836 })
837}
838
839fn get_prop(recv: &Value, key: &str) -> Option<Value> {
840 with_host(|h| match h.get(recv) {
841 Some(JsObj::Object(p)) => p.get(key).cloned(),
842 _ => None,
843 })
844}
845
846fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
847 get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
848}
849
850pub fn request(args: &[Value], is_get: bool) -> Result<Value, String> {
853 let mut host = "localhost".to_string();
854 let mut port: u16 = 80;
855 let mut path = "/".to_string();
856 let mut method = "GET".to_string();
857 let mut headers: Vec<(String, String)> = Vec::new();
858 let mut cb: Option<Value> = None;
859
860 for a in args {
861 if with_host(|h| crate::host::is_callable(h, a)) {
862 cb = Some(a.clone());
863 } else if with_host(|h| h.as_str(a)).is_some() {
864 let url = with_host(|h| h.str_of(a));
865 parse_url(&url, &mut host, &mut port, &mut path);
866 } else if matches!(a, Value::Obj(_)) {
867 for key in ["hostname", "host"] {
868 if let Some(v) = get_prop(a, key).filter(|v| with_host(|h| h.as_str(v)).is_some()) {
869 host = with_host(|h| h.str_of(&v));
870 }
871 }
872 if let Some(v) = get_prop(a, "port") {
873 let n = with_host(|h| h.to_number(&v));
874 if !n.is_nan() {
875 port = n as u16;
876 }
877 }
878 if let Some(v) = get_prop(a, "path").filter(|v| with_host(|h| h.as_str(v)).is_some()) {
879 path = with_host(|h| h.str_of(&v));
880 }
881 if let Some(v) = get_prop(a, "method").filter(|v| with_host(|h| h.as_str(v)).is_some())
882 {
883 method = with_host(|h| h.str_of(&v));
884 }
885 if let Some(hv) = get_prop(a, "headers").filter(|v| matches!(v, Value::Obj(_))) {
886 for (k, val) in object_pairs(&hv) {
887 headers.push((k, val));
888 }
889 }
890 }
891 }
892 if is_get {
893 method = "GET".to_string();
894 }
895
896 let reqid = next_reqid();
897 let mut extra = IndexMap::new();
898 extra.insert("@@reqid".into(), Value::Float(reqid as f64));
899 extra.insert("method".into(), with_host(|h| h.new_str(method.clone())));
900 extra.insert("path".into(), with_host(|h| h.new_str(path.clone())));
901 let request = super::net::new_emitter_object("ClientRequest", extra);
902 if let Some(cb) = cb {
904 super::events::instance_call(
905 &request,
906 "on",
907 vec![with_host(|h| h.new_str("response")), cb],
908 )?;
909 }
910 CLIENT_REQS.with(|c| {
911 c.borrow_mut().insert(
912 reqid,
913 ClientReq {
914 host,
915 port,
916 method,
917 path,
918 headers,
919 body: Vec::new(),
920 request: request.clone(),
921 sent: false,
922 },
923 );
924 });
925 if is_get {
926 dispatch_request(reqid)?;
927 }
928 Ok(request)
929}
930
931fn parse_url(url: &str, host: &mut String, port: &mut u16, path: &mut String) {
933 let rest = url.strip_prefix("http://").unwrap_or(url);
934 let (authority, p) = match rest.find('/') {
935 Some(i) => (&rest[..i], &rest[i..]),
936 None => (rest, "/"),
937 };
938 *path = if p.is_empty() {
939 "/".to_string()
940 } else {
941 p.to_string()
942 };
943 if let Some((h, port_str)) = authority.rsplit_once(':') {
944 *host = h.to_string();
945 if let Ok(n) = port_str.parse::<u16>() {
946 *port = n;
947 }
948 } else {
949 *host = authority.to_string();
950 *port = 80;
951 }
952}
953
954fn client_request_call(req: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
955 let reqid = u64_prop(req, "@@reqid");
956 match method {
957 "write" => {
958 if let Some(id) = reqid {
959 let bytes = value_bytes(args.first());
960 CLIENT_REQS.with(|c| {
961 if let Some(r) = c.borrow_mut().get_mut(&id) {
962 r.body.extend_from_slice(&bytes);
963 }
964 });
965 }
966 Ok(Value::Bool(true))
967 }
968 "end" => {
969 if let Some(id) = reqid {
970 if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
971 let bytes = value_bytes(Some(chunk));
972 CLIENT_REQS.with(|c| {
973 if let Some(r) = c.borrow_mut().get_mut(&id) {
974 r.body.extend_from_slice(&bytes);
975 }
976 });
977 }
978 dispatch_request(id)?;
979 }
980 Ok(req.clone())
981 }
982 "setHeader" => {
983 if let Some(id) = reqid {
984 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
985 let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
986 CLIENT_REQS.with(|c| {
987 if let Some(r) = c.borrow_mut().get_mut(&id) {
988 upsert_header(&mut r.headers, &k, v);
989 }
990 });
991 }
992 Ok(Value::Undef)
993 }
994 "getHeader" => {
995 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
996 .to_ascii_lowercase();
997 let val = reqid.and_then(|id| {
998 CLIENT_REQS.with(|c| {
999 c.borrow().get(&id).and_then(|r| {
1000 r.headers
1001 .iter()
1002 .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
1003 .map(|(_, v)| v.clone())
1004 })
1005 })
1006 });
1007 Ok(val
1008 .map(|v| with_host(|h| h.new_str(v)))
1009 .unwrap_or(Value::Undef))
1010 }
1011 "removeHeader" => {
1012 if let Some(id) = reqid {
1013 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1014 CLIENT_REQS.with(|c| {
1015 if let Some(r) = c.borrow_mut().get_mut(&id) {
1016 r.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
1017 }
1018 });
1019 }
1020 Ok(Value::Undef)
1021 }
1022 "abort" | "destroy" | "setTimeout" | "flushHeaders" => Ok(req.clone()),
1023 _ => Err(crate::host::type_error(&format!(
1024 "req.{method} is not a function"
1025 ))),
1026 }
1027}
1028
1029fn dispatch_request(reqid: u64) -> Result<(), String> {
1032 let sent = CLIENT_REQS.with(|c| c.borrow().get(&reqid).map(|r| r.sent).unwrap_or(true));
1033 if sent {
1034 return Ok(());
1035 }
1036 CLIENT_REQS.with(|c| {
1037 if let Some(r) = c.borrow_mut().get_mut(&reqid) {
1038 r.sent = true;
1039 }
1040 });
1041
1042 let (host, port, method, path, headers, body) = CLIENT_REQS.with(|c| {
1043 let b = c.borrow();
1044 let r = b.get(&reqid).unwrap();
1045 (
1046 r.host.clone(),
1047 r.port,
1048 r.method.clone(),
1049 r.path.clone(),
1050 r.headers.clone(),
1051 r.body.clone(),
1052 )
1053 });
1054
1055 let io_tx = with_host(|h| h.io_sender());
1056 with_host(|h| h.incr_handle());
1057
1058 let mut has_host = false;
1060 let mut has_len = false;
1061 let mut header_block = String::new();
1062 for (k, v) in &headers {
1063 if k.eq_ignore_ascii_case("host") {
1064 has_host = true;
1065 }
1066 if k.eq_ignore_ascii_case("content-length") {
1067 has_len = true;
1068 }
1069 if k.eq_ignore_ascii_case("connection") {
1070 continue;
1071 }
1072 header_block.push_str(&format!("{k}: {v}\r\n"));
1073 }
1074 let host_header = if port == 80 {
1075 host.clone()
1076 } else {
1077 format!("{host}:{port}")
1078 };
1079 let mut request_bytes = format!("{method} {path} HTTP/1.1\r\n");
1080 if !has_host {
1081 request_bytes.push_str(&format!("Host: {host_header}\r\n"));
1082 }
1083 request_bytes.push_str(&header_block);
1084 if !has_len && !body.is_empty() {
1085 request_bytes.push_str(&format!("Content-Length: {}\r\n", body.len()));
1086 }
1087 request_bytes.push_str("Connection: close\r\n\r\n");
1088 let mut wire = request_bytes.into_bytes();
1089 wire.extend_from_slice(&body);
1090
1091 std::thread::spawn(move || match exchange(&host, port, &wire) {
1092 Ok(raw) => {
1093 let _ = io_tx.send(Box::new(move || deliver_response(reqid, raw)));
1094 }
1095 Err(msg) => {
1096 let _ = io_tx.send(Box::new(move || deliver_error(reqid, msg)));
1097 }
1098 });
1099 Ok(())
1100}
1101
1102pub(crate) fn exchange(host: &str, port: u16, request: &[u8]) -> Result<Vec<u8>, String> {
1104 let mut stream = TcpStream::connect((host, port))
1105 .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
1106 stream
1107 .write_all(request)
1108 .map_err(|e| format!("Error: http write: {e}"))?;
1109 stream
1110 .flush()
1111 .map_err(|e| format!("Error: http flush: {e}"))?;
1112 let mut raw = Vec::new();
1113 let mut buf = [0u8; 16384];
1114 loop {
1115 match stream.read(&mut buf) {
1116 Ok(0) => break,
1117 Ok(n) => raw.extend_from_slice(&buf[..n]),
1118 Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1119 Err(e) => {
1120 if raw.is_empty() {
1121 return Err(format!("Error: http read: {e}"));
1122 }
1123 break;
1124 }
1125 }
1126 }
1127 Ok(raw)
1128}
1129
1130fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
1133 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1134 with_host(|h| h.decr_handle());
1135 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1136 let Some(entry) = entry else { return Ok(()) };
1137
1138 let (status, message, http_version, headers, body) = parse_raw_response(&raw);
1139
1140 let headers_obj = with_host(|h| {
1141 let mut m = IndexMap::new();
1142 for (k, v) in &headers {
1143 m.insert(k.clone(), h.new_str(v.clone()));
1144 }
1145 h.new_object(m)
1146 });
1147 let mut extra = IndexMap::new();
1148 extra.insert("statusCode".into(), Value::Float(status as f64));
1149 extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
1150 extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
1151 extra.insert("headers".into(), headers_obj);
1152 let res = super::net::new_emitter_object("IncomingMessage", extra);
1153
1154 super::events::instance_call(
1155 &entry.request,
1156 "emit",
1157 vec![with_host(|h| h.new_str("response")), res.clone()],
1158 )?;
1159 if !body.is_empty() {
1160 let chunk = super::buffer::from_bytes(&body);
1161 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
1162 }
1163 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
1164 Ok(())
1165}
1166
1167fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
1169 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1170 with_host(|h| h.decr_handle());
1171 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1172 if let Some(entry) = entry {
1173 let err = with_host(|h| {
1174 let mut m = IndexMap::new();
1175 m.insert("message".into(), h.new_str(msg.clone()));
1176 h.new_object(m)
1177 });
1178 super::events::instance_call(
1179 &entry.request,
1180 "emit",
1181 vec![with_host(|h| h.new_str("error")), err],
1182 )?;
1183 }
1184 Ok(())
1185}
1186
1187pub(crate) fn parse_raw_response(
1190 raw: &[u8],
1191) -> (u16, String, String, Vec<(String, String)>, Vec<u8>) {
1192 let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
1193 let head = String::from_utf8_lossy(&raw[..head_end]);
1194 let body_start = (head_end + 4).min(raw.len());
1195 let mut lines = head.split("\r\n");
1196 let status_line = lines.next().unwrap_or("");
1197 let mut sp = status_line.splitn(3, ' ');
1198 let version = sp
1199 .next()
1200 .unwrap_or("HTTP/1.1")
1201 .strip_prefix("HTTP/")
1202 .unwrap_or("1.1")
1203 .to_string();
1204 let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
1205 let message = sp.next().unwrap_or("").to_string();
1206
1207 let mut headers: Vec<(String, String)> = Vec::new();
1208 let mut chunked = false;
1209 for line in lines {
1210 if line.is_empty() {
1211 continue;
1212 }
1213 if let Some((k, v)) = line.split_once(':') {
1214 let name = k.trim().to_ascii_lowercase();
1215 let value = v.trim().to_string();
1216 if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1217 chunked = true;
1218 }
1219 headers.push((name, value));
1220 }
1221 }
1222 let raw_body = &raw[body_start..];
1223 let body = if chunked {
1224 decode_chunked(raw_body)
1225 } else {
1226 raw_body.to_vec()
1227 };
1228 (status, message, version, headers, body)
1229}
1230
1231fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1234 let mut out = Vec::new();
1235 while let Some(nl) = find_subslice(data, b"\r\n") {
1236 let size_line = String::from_utf8_lossy(&data[..nl]);
1237 let size_hex = size_line.split(';').next().unwrap_or("").trim();
1238 let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1239 if size == 0 {
1240 break;
1241 }
1242 let chunk_start = nl + 2;
1243 let chunk_end = (chunk_start + size).min(data.len());
1244 out.extend_from_slice(&data[chunk_start..chunk_end]);
1245 let next = chunk_end + 2;
1246 if next >= data.len() {
1247 break;
1248 }
1249 data = &data[next..];
1250 }
1251 out
1252}
1253
1254fn status_text(code: u16) -> &'static str {
1256 match code {
1257 200 => "OK",
1258 201 => "Created",
1259 202 => "Accepted",
1260 204 => "No Content",
1261 301 => "Moved Permanently",
1262 302 => "Found",
1263 304 => "Not Modified",
1264 400 => "Bad Request",
1265 401 => "Unauthorized",
1266 403 => "Forbidden",
1267 404 => "Not Found",
1268 405 => "Method Not Allowed",
1269 409 => "Conflict",
1270 500 => "Internal Server Error",
1271 502 => "Bad Gateway",
1272 503 => "Service Unavailable",
1273 _ => "OK",
1274 }
1275}