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> {
1115 let mut stream = TcpStream::connect((host, port))
1116 .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
1117 stream
1118 .write_all(request)
1119 .map_err(|e| format!("Error: http write: {e}"))?;
1120 stream
1121 .flush()
1122 .map_err(|e| format!("Error: http flush: {e}"))?;
1123 let head_request = request
1124 .split(|b| *b == b' ')
1125 .next()
1126 .is_some_and(|m| m.eq_ignore_ascii_case(b"HEAD"));
1127 let mut raw = Vec::new();
1128 let mut buf = [0u8; 16384];
1129 loop {
1130 if response_is_complete(&raw, head_request) {
1131 break;
1132 }
1133 match stream.read(&mut buf) {
1134 Ok(0) => break,
1135 Ok(n) => raw.extend_from_slice(&buf[..n]),
1136 Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1137 Err(e) => {
1138 if raw.is_empty() {
1139 return Err(format!("Error: http read: {e}"));
1140 }
1141 break;
1142 }
1143 }
1144 }
1145 Ok(raw)
1146}
1147
1148pub(crate) fn response_is_complete(raw: &[u8], head_request: bool) -> bool {
1154 let Some(head_end) = find_subslice(raw, b"\r\n\r\n") else {
1155 return false;
1156 };
1157 let head = String::from_utf8_lossy(&raw[..head_end]);
1158 let mut lines = head.split("\r\n");
1159 let status = lines
1160 .next()
1161 .and_then(|l| l.split(' ').nth(1).and_then(|s| s.parse::<u16>().ok()))
1162 .unwrap_or(0);
1163 if (100..200).contains(&status) {
1166 return false;
1167 }
1168 if head_request || status == 204 || status == 304 {
1170 return true;
1171 }
1172 let mut chunked = false;
1173 let mut content_length: Option<usize> = None;
1174 for line in lines {
1175 let Some((k, v)) = line.split_once(':') else {
1176 continue;
1177 };
1178 let name = k.trim().to_ascii_lowercase();
1179 let value = v.trim();
1180 if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1181 chunked = true;
1182 } else if name == "content-length" {
1183 content_length = value.parse().ok();
1184 }
1185 }
1186 let body = &raw[head_end + 4..];
1187 if chunked {
1189 return chunked_body_is_terminated(body);
1190 }
1191 match content_length {
1192 Some(n) => body.len() >= n,
1193 None => false,
1194 }
1195}
1196
1197fn chunked_body_is_terminated(mut data: &[u8]) -> bool {
1200 loop {
1201 let Some(nl) = find_subslice(data, b"\r\n") else {
1202 return false;
1203 };
1204 let size_line = String::from_utf8_lossy(&data[..nl]);
1205 let Ok(size) = usize::from_str_radix(size_line.split(';').next().unwrap_or("").trim(), 16)
1206 else {
1207 return false;
1208 };
1209 if size == 0 {
1210 return find_subslice(&data[nl + 2..], b"\r\n").is_some();
1212 }
1213 let consumed = nl + 2 + size + 2;
1215 if data.len() < consumed {
1216 return false;
1217 }
1218 data = &data[consumed..];
1219 }
1220}
1221
1222fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
1225 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1226 with_host(|h| h.decr_handle());
1227 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1228 let Some(entry) = entry else { return Ok(()) };
1229
1230 let (status, message, http_version, headers, body) = parse_raw_response(&raw);
1231
1232 let headers_obj = with_host(|h| {
1233 let mut m = IndexMap::new();
1234 for (k, v) in &headers {
1235 m.insert(k.clone(), h.new_str(v.clone()));
1236 }
1237 h.new_object(m)
1238 });
1239 let mut extra = IndexMap::new();
1240 extra.insert("statusCode".into(), Value::Float(status as f64));
1241 extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
1242 extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
1243 extra.insert("headers".into(), headers_obj);
1244 let res = super::net::new_emitter_object("IncomingMessage", extra);
1245
1246 super::events::instance_call(
1247 &entry.request,
1248 "emit",
1249 vec![with_host(|h| h.new_str("response")), res.clone()],
1250 )?;
1251 if !body.is_empty() {
1252 let chunk = super::buffer::from_bytes(&body);
1253 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
1254 }
1255 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
1256 Ok(())
1257}
1258
1259fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
1261 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1262 with_host(|h| h.decr_handle());
1263 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1264 if let Some(entry) = entry {
1265 let err = with_host(|h| {
1266 let mut m = IndexMap::new();
1267 m.insert("message".into(), h.new_str(msg.clone()));
1268 h.new_object(m)
1269 });
1270 super::events::instance_call(
1271 &entry.request,
1272 "emit",
1273 vec![with_host(|h| h.new_str("error")), err],
1274 )?;
1275 }
1276 Ok(())
1277}
1278
1279pub(crate) fn parse_raw_response(
1282 raw: &[u8],
1283) -> (u16, String, String, Vec<(String, String)>, Vec<u8>) {
1284 let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
1285 let head = String::from_utf8_lossy(&raw[..head_end]);
1286 let body_start = (head_end + 4).min(raw.len());
1287 let mut lines = head.split("\r\n");
1288 let status_line = lines.next().unwrap_or("");
1289 let mut sp = status_line.splitn(3, ' ');
1290 let version = sp
1291 .next()
1292 .unwrap_or("HTTP/1.1")
1293 .strip_prefix("HTTP/")
1294 .unwrap_or("1.1")
1295 .to_string();
1296 let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
1297 let message = sp.next().unwrap_or("").to_string();
1298
1299 let mut headers: Vec<(String, String)> = Vec::new();
1300 let mut chunked = false;
1301 for line in lines {
1302 if line.is_empty() {
1303 continue;
1304 }
1305 if let Some((k, v)) = line.split_once(':') {
1306 let name = k.trim().to_ascii_lowercase();
1307 let value = v.trim().to_string();
1308 if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1309 chunked = true;
1310 }
1311 headers.push((name, value));
1312 }
1313 }
1314 let raw_body = &raw[body_start..];
1315 let body = if chunked {
1316 decode_chunked(raw_body)
1317 } else {
1318 raw_body.to_vec()
1319 };
1320 (status, message, version, headers, body)
1321}
1322
1323fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1326 let mut out = Vec::new();
1327 while let Some(nl) = find_subslice(data, b"\r\n") {
1328 let size_line = String::from_utf8_lossy(&data[..nl]);
1329 let size_hex = size_line.split(';').next().unwrap_or("").trim();
1330 let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1331 if size == 0 {
1332 break;
1333 }
1334 let chunk_start = nl + 2;
1335 let chunk_end = (chunk_start + size).min(data.len());
1336 out.extend_from_slice(&data[chunk_start..chunk_end]);
1337 let next = chunk_end + 2;
1338 if next >= data.len() {
1339 break;
1340 }
1341 data = &data[next..];
1342 }
1343 out
1344}
1345
1346fn status_text(code: u16) -> &'static str {
1348 match code {
1349 200 => "OK",
1350 201 => "Created",
1351 202 => "Accepted",
1352 204 => "No Content",
1353 301 => "Moved Permanently",
1354 302 => "Found",
1355 304 => "Not Modified",
1356 400 => "Bad Request",
1357 401 => "Unauthorized",
1358 403 => "Forbidden",
1359 404 => "Not Found",
1360 405 => "Method Not Allowed",
1361 409 => "Conflict",
1362 500 => "Internal Server Error",
1363 502 => "Bad Gateway",
1364 503 => "Service Unavailable",
1365 _ => "OK",
1366 }
1367}
1368
1369#[cfg(test)]
1370mod framing_tests {
1371 use super::response_is_complete;
1372
1373 #[test]
1376 fn a_keep_alive_content_length_response_is_complete_at_its_last_body_byte() {
1377 let head = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\
1378 Content-Length: 12\r\nConnection: keep-alive\r\n\r\n";
1379 let mut raw = head.to_vec();
1380 assert!(!response_is_complete(&raw, false), "no body yet");
1381 raw.extend_from_slice(b"hello GET /");
1382 assert!(!response_is_complete(&raw, false), "11 of 12 bytes");
1383 raw.push(b'p');
1384 assert!(response_is_complete(&raw, false), "12 of 12 bytes");
1385 }
1386
1387 #[test]
1388 fn a_partial_header_block_is_never_complete() {
1389 assert!(!response_is_complete(b"", false));
1390 assert!(!response_is_complete(b"HTTP/1.1 200 OK\r\n", false));
1391 assert!(!response_is_complete(
1392 b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n",
1393 false
1394 ));
1395 }
1396
1397 #[test]
1400 fn an_unframed_response_is_never_reported_complete() {
1401 let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nbody bytes";
1402 assert!(!response_is_complete(raw, false));
1403 }
1404
1405 #[test]
1406 fn a_chunked_body_completes_only_at_its_zero_chunk() {
1407 let head = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n";
1408 let mut raw = head.to_vec();
1409 assert!(!response_is_complete(&raw, false));
1410 raw.extend_from_slice(b"5\r\nhello\r\n");
1411 assert!(
1412 !response_is_complete(&raw, false),
1413 "one chunk, no terminator"
1414 );
1415 raw.extend_from_slice(b"0\r\n");
1416 assert!(
1417 !response_is_complete(&raw, false),
1418 "trailer CRLF still missing"
1419 );
1420 raw.extend_from_slice(b"\r\n");
1421 assert!(response_is_complete(&raw, false));
1422 }
1423
1424 #[test]
1427 fn chunk_extensions_parse_and_chunked_outranks_content_length() {
1428 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 99\r\nTransfer-Encoding: chunked\r\n\r\n\
1429 3;name=v\r\nabc\r\n0\r\n\r\n";
1430 assert!(response_is_complete(raw, false));
1431 let short =
1432 b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nTransfer-Encoding: chunked\r\n\r\nabc";
1433 assert!(
1434 !response_is_complete(short, false),
1435 "Content-Length must not settle a chunked response"
1436 );
1437 }
1438
1439 #[test]
1442 fn bodiless_statuses_and_head_finish_at_the_header_block() {
1443 assert!(response_is_complete(
1444 b"HTTP/1.1 204 No Content\r\nContent-Length: 7\r\n\r\n",
1445 false
1446 ));
1447 assert!(response_is_complete(
1448 b"HTTP/1.1 304 Not Modified\r\nContent-Length: 7\r\n\r\n",
1449 false
1450 ));
1451 assert!(response_is_complete(
1452 b"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\n",
1453 true
1454 ));
1455 assert!(
1456 !response_is_complete(b"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\n", false),
1457 "the same response to a GET still needs its body"
1458 );
1459 }
1460
1461 #[test]
1463 fn an_interim_response_does_not_end_the_read() {
1464 assert!(!response_is_complete(
1465 b"HTTP/1.1 100 Continue\r\n\r\n",
1466 false
1467 ));
1468 assert!(!response_is_complete(
1469 b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n",
1470 false
1471 ));
1472 }
1473}