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 head: bool,
197 status: u16,
199 message: Option<String>,
201 headers: Vec<(String, String)>,
204 body: Vec<u8>,
206}
207
208thread_local! {
209 static CONNS: std::cell::RefCell<HashMap<u64, HttpConn>> =
210 std::cell::RefCell::new(HashMap::new());
211 static RESPONSES: std::cell::RefCell<HashMap<u64, ResState>> =
212 std::cell::RefCell::new(HashMap::new());
213 static NEXT_RESID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
214}
215
216fn next_resid() -> u64 {
217 NEXT_RESID.with(|c| {
218 let id = c.get();
219 c.set(id + 1);
220 id
221 })
222}
223
224pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
228 match method {
229 "createServer" => Some(Ok(create_server(args.first().cloned()))),
230 "request" => Some(request(args, false)),
231 "get" => Some(request(args, true)),
232 "validateHeaderName" => Some(validate_header_name(args)),
233 "validateHeaderValue" => Some(validate_header_value(args)),
234 "setMaxIdleHTTPParsers" | "setGlobalProxyFromEnv" => Some(Ok(Value::Undef)),
236 _ => None,
237 }
238}
239
240fn validate_header_name(args: &[Value]) -> Result<Value, String> {
243 let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
244 if is_valid_token(&name) {
245 Ok(Value::Undef)
246 } else {
247 Err(crate::host::coded_error(
248 "TypeError",
249 "ERR_INVALID_HTTP_TOKEN",
250 &format!("Header name must be a valid HTTP token [\"{name}\"]"),
251 ))
252 }
253}
254
255fn validate_header_value(args: &[Value]) -> Result<Value, String> {
259 let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
260 let raw = args.get(1).cloned().unwrap_or(Value::Undef);
261 if matches!(raw, Value::Undef) {
262 return Err(crate::host::coded_error(
263 "TypeError",
264 "ERR_HTTP_INVALID_HEADER_VALUE",
265 &format!("Invalid value \"undefined\" for header \"{name}\""),
266 ));
267 }
268 let value = with_host(|h| h.str_of(&raw));
269 if value.bytes().any(|b| b != b'\t' && (b < 0x20 || b == 0x7f)) {
270 return Err(crate::host::coded_error(
271 "TypeError",
272 "ERR_INVALID_CHAR",
273 &format!("Invalid character in header content [\"{name}\"]"),
274 ));
275 }
276 Ok(Value::Undef)
277}
278
279fn is_valid_token(s: &str) -> bool {
281 !s.is_empty()
282 && s.bytes().all(|b| {
283 b.is_ascii_alphanumeric()
284 || matches!(
285 b,
286 b'!' | b'#'
287 | b'$'
288 | b'%'
289 | b'&'
290 | b'\''
291 | b'*'
292 | b'+'
293 | b'-'
294 | b'.'
295 | b'^'
296 | b'_'
297 | b'`'
298 | b'|'
299 | b'~'
300 )
301 })
302}
303
304pub fn construct_agent(args: &[Value]) -> Value {
308 let pairs = args
309 .first()
310 .filter(|v| matches!(v, Value::Obj(_)))
311 .map(object_pairs)
312 .unwrap_or_default();
313 with_host(|h| {
314 let mut m = IndexMap::new();
315 m.insert("@@native".into(), h.new_str("Agent"));
316 m.insert("maxSockets".into(), Value::Float(f64::INFINITY));
317 m.insert("maxFreeSockets".into(), Value::Float(256.0));
318 m.insert("sockets".into(), h.new_object(IndexMap::new()));
319 m.insert("requests".into(), h.new_object(IndexMap::new()));
320 for (k, v) in pairs {
321 m.insert(k, h.new_str(v));
322 }
323 h.new_object(m)
324 })
325}
326
327pub fn create_server(request_listener: Option<Value>) -> Value {
330 let server = super::net::create_server(None);
331 let listener = request_listener.unwrap_or(Value::Undef);
332 let hook = std::rc::Rc::new(
333 move |_server: &Value, socket: &Value| -> Result<(), String> {
334 let sock_id = socket_id_of(socket);
335 CONNS.with(|c| {
336 c.borrow_mut().insert(
337 sock_id,
338 HttpConn {
339 socket: socket.clone(),
340 listener: listener.clone(),
341 buf: Vec::new(),
342 },
343 );
344 });
345 Ok(())
346 },
347 );
348 super::net::set_conn_hook(&server, hook);
349 server
350}
351
352fn socket_id_of(socket: &Value) -> u64 {
353 with_host(|h| match h.get(socket) {
354 Some(JsObj::Object(p)) => p.get("@@netid").map(|v| h.to_number(v) as u64).unwrap_or(0),
355 _ => 0,
356 })
357}
358
359pub fn drop_conn(sock_id: u64) {
361 CONNS.with(|c| {
362 c.borrow_mut().remove(&sock_id);
363 });
364}
365
366pub fn feed(sock_id: u64, _socket: &Value, bytes: &[u8]) -> Result<(), String> {
371 let is_http = CONNS.with(|c| c.borrow().contains_key(&sock_id));
372 if !is_http {
373 return Ok(());
374 }
375 CONNS.with(|c| {
376 c.borrow_mut()
377 .get_mut(&sock_id)
378 .unwrap()
379 .buf
380 .extend_from_slice(bytes)
381 });
382
383 loop {
385 let (listener, socket, parsed) = CONNS.with(|c| {
386 let mut c = c.borrow_mut();
387 let conn = c.get_mut(&sock_id).unwrap();
388 match parse_request(&conn.buf) {
389 Some((req, consumed)) => {
390 conn.buf.drain(..consumed);
391 (conn.listener.clone(), conn.socket.clone(), Some(req))
392 }
393 None => (Value::Undef, Value::Undef, None),
394 }
395 });
396 let Some(parsed) = parsed else { break };
397
398 let req = build_incoming(&parsed);
399 let res = build_response(sock_id, parsed.method.eq_ignore_ascii_case("HEAD"));
400 if with_host(|h| crate::host::is_callable(h, &listener)) {
401 invoke(&listener, vec![req.clone(), res], None)?;
402 }
403 if !parsed.body.is_empty() {
407 let encoding = with_host(|h| match h.get(&req) {
408 Some(JsObj::Object(p)) => p.get("@@encoding").map(|v| h.str_of(v)),
409 _ => None,
410 });
411 let chunk = match encoding {
412 Some(enc) => with_host(|h| {
413 let s = super::buffer::encode_bytes(&parsed.body, &enc);
414 h.new_str(s)
415 }),
416 None => super::buffer::from_bytes(&parsed.body),
417 };
418 super::events::instance_call(
419 &req,
420 "emit",
421 vec![with_host(|h| h.new_str("data")), chunk],
422 )?;
423 }
424 super::events::instance_call(&req, "emit", vec![with_host(|h| h.new_str("end"))])?;
425 let _ = socket; }
427 Ok(())
428}
429
430struct ParsedReq {
432 method: String,
433 url: String,
434 http_version: String,
435 headers: Vec<(String, String)>,
437 body: Vec<u8>,
438}
439
440fn parse_request(buf: &[u8]) -> Option<(ParsedReq, usize)> {
443 let head_end = find_subslice(buf, b"\r\n\r\n")?;
445 let head = &buf[..head_end];
446 let body_start = head_end + 4;
447
448 let head_str = String::from_utf8_lossy(head);
449 let mut lines = head_str.split("\r\n");
450 let request_line = lines.next()?;
451 let mut parts = request_line.split(' ');
452 let method = parts.next()?.to_string();
453 let url = parts.next()?.to_string();
454 let version = parts.next().unwrap_or("HTTP/1.1");
455 let http_version = version.strip_prefix("HTTP/").unwrap_or("1.1").to_string();
456
457 let mut headers: Vec<(String, String)> = Vec::new();
458 let mut content_length = 0usize;
459 for line in lines {
460 if line.is_empty() {
461 continue;
462 }
463 if let Some((k, v)) = line.split_once(':') {
464 let name = k.trim().to_ascii_lowercase();
465 let value = v.trim().to_string();
466 if name == "content-length" {
467 content_length = value.parse().unwrap_or(0);
468 }
469 headers.push((name, value));
470 }
471 }
472
473 if buf.len() < body_start + content_length {
475 return None;
476 }
477 let body = buf[body_start..body_start + content_length].to_vec();
478 Some((
479 ParsedReq {
480 method,
481 url,
482 http_version,
483 headers,
484 body,
485 },
486 body_start + content_length,
487 ))
488}
489
490fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
491 haystack.windows(needle.len()).position(|w| w == needle)
492}
493
494fn build_incoming(req: &ParsedReq) -> Value {
497 let headers_obj = with_host(|h| {
498 let mut m = IndexMap::new();
499 for (k, v) in &req.headers {
500 m.insert(k.clone(), h.new_str(v.clone()));
501 }
502 h.new_object(m)
503 });
504 let mut extra = IndexMap::new();
505 extra.insert(
506 "method".into(),
507 with_host(|h| h.new_str(req.method.clone())),
508 );
509 extra.insert("url".into(), with_host(|h| h.new_str(req.url.clone())));
510 extra.insert(
511 "httpVersion".into(),
512 with_host(|h| h.new_str(req.http_version.clone())),
513 );
514 extra.insert("headers".into(), headers_obj);
515 super::net::new_emitter_object("IncomingMessage", extra)
516}
517
518fn incoming_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
525 match method {
526 "pause" | "resume" | "unpipe" | "destroy" => Ok(recv.clone()),
530 "isPaused" => Ok(Value::Bool(false)),
531 "setEncoding" => {
533 let enc = super::arg_str(&args, 0);
534 with_host(|h| {
535 let v = h.new_str(enc);
536 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
537 p.insert("@@encoding".into(), v);
538 }
539 });
540 Ok(recv.clone())
541 }
542 _ => Err(crate::host::type_error(&format!(
543 "req.{method} is not a function"
544 ))),
545 }
546}
547
548fn build_response(sock_id: u64, head: bool) -> Value {
551 let resid = next_resid();
552 RESPONSES.with(|r| {
553 r.borrow_mut().insert(
554 resid,
555 ResState {
556 sock_id,
557 head,
558 status: 200,
559 message: None,
560 headers: Vec::new(),
561 body: Vec::new(),
562 },
563 );
564 });
565 let mut extra = IndexMap::new();
566 extra.insert("@@resid".into(), Value::Float(resid as f64));
567 extra.insert("statusCode".into(), Value::Float(200.0));
568 super::net::new_emitter_object("ServerResponse", extra)
569}
570
571fn resid_of(res: &Value) -> Option<u64> {
572 with_host(|h| match h.get(res) {
573 Some(JsObj::Object(p)) => p.get("@@resid").map(|v| h.to_number(v) as u64),
574 _ => None,
575 })
576}
577
578pub fn instance_call(
581 tag: &str,
582 recv: &Value,
583 method: &str,
584 args: Vec<Value>,
585) -> Result<Value, String> {
586 if super::events::METHODS.contains(&method) {
592 return super::events::instance_call(recv, method, args);
593 }
594 match tag {
595 "IncomingMessage" => incoming_call(recv, method, args),
596 "ServerResponse" => response_call(recv, method, args),
597 "ClientRequest" => client_request_call(recv, method, args),
598 "Agent" => match method {
600 "destroy" => Ok(Value::Undef),
601 "getName" => Ok(with_host(|h| h.new_str("localhost:80:"))),
602 _ => Err(crate::host::type_error(&format!(
603 "agent.{method} is not a function"
604 ))),
605 },
606 _ => Err(crate::host::type_error(&format!(
607 "{method} is not a function"
608 ))),
609 }
610}
611
612fn response_call(res: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
613 let Some(resid) = resid_of(res) else {
614 return Err(crate::host::type_error("invalid ServerResponse"));
615 };
616 match method {
617 "writeHead" => {
618 let status =
619 with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u16;
620 let mut message: Option<String> = None;
622 let mut headers_arg: Option<Value> = None;
623 if let Some(a) = args.get(1) {
624 if with_host(|h| h.as_str(a)).is_some() {
625 message = Some(with_host(|h| h.str_of(a)));
626 } else if !matches!(a, Value::Undef) {
627 headers_arg = Some(a.clone());
628 }
629 }
630 if let Some(a) = args.get(2) {
631 if !matches!(a, Value::Undef) {
632 headers_arg = Some(a.clone());
633 }
634 }
635 let header_pairs = headers_arg.map(|h| object_pairs(&h)).unwrap_or_default();
636 RESPONSES.with(|r| {
637 if let Some(st) = r.borrow_mut().get_mut(&resid) {
638 st.status = status;
639 st.message = message;
640 for (k, v) in header_pairs {
641 upsert_header(&mut st.headers, &k, v);
642 }
643 }
644 });
645 set_res_prop(res, "statusCode", Value::Float(status as f64));
647 Ok(res.clone())
648 }
649 "setHeader" => {
650 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
651 let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
652 RESPONSES.with(|r| {
653 if let Some(st) = r.borrow_mut().get_mut(&resid) {
654 upsert_header(&mut st.headers, &k, v);
655 }
656 });
657 Ok(Value::Undef)
658 }
659 "getHeader" => {
660 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
661 .to_ascii_lowercase();
662 let val = RESPONSES.with(|r| {
663 r.borrow().get(&resid).and_then(|st| {
664 st.headers
665 .iter()
666 .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
667 .map(|(_, v)| v.clone())
668 })
669 });
670 Ok(val
671 .map(|v| with_host(|h| h.new_str(v)))
672 .unwrap_or(Value::Undef))
673 }
674 "removeHeader" => {
675 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
676 RESPONSES.with(|r| {
677 if let Some(st) = r.borrow_mut().get_mut(&resid) {
678 st.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
679 }
680 });
681 Ok(Value::Undef)
682 }
683 "write" => {
684 let bytes = value_bytes(args.first());
685 RESPONSES.with(|r| {
686 if let Some(st) = r.borrow_mut().get_mut(&resid) {
687 st.body.extend_from_slice(&bytes);
688 }
689 });
690 Ok(Value::Bool(true))
691 }
692 "end" => {
693 if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
694 let bytes = value_bytes(Some(chunk));
695 RESPONSES.with(|r| {
696 if let Some(st) = r.borrow_mut().get_mut(&resid) {
697 st.body.extend_from_slice(&bytes);
698 }
699 });
700 }
701 finish_response(res, resid)?;
702 Ok(res.clone())
703 }
704 "hasHeader" => {
716 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
717 let found = RESPONSES.with(|r| {
718 r.borrow()
719 .get(&resid)
720 .is_some_and(|st| st.headers.iter().any(|(hk, _)| hk.eq_ignore_ascii_case(&k)))
721 });
722 Ok(Value::Bool(found))
723 }
724 "getHeaderNames" => {
725 let names = RESPONSES.with(|r| {
726 r.borrow()
727 .get(&resid)
728 .map(|st| {
729 st.headers
730 .iter()
731 .map(|(k, _)| k.to_ascii_lowercase())
732 .collect::<Vec<_>>()
733 })
734 .unwrap_or_default()
735 });
736 Ok(with_host(|h| {
737 let items = names.into_iter().map(|n| h.new_str(n)).collect::<Vec<_>>();
738 h.new_array(items)
739 }))
740 }
741 "getHeaders" => {
742 let pairs = RESPONSES.with(|r| {
743 r.borrow()
744 .get(&resid)
745 .map(|st| {
746 st.headers
747 .iter()
748 .map(|(k, v)| (k.to_ascii_lowercase(), v.clone()))
749 .collect::<Vec<_>>()
750 })
751 .unwrap_or_default()
752 });
753 Ok(with_host(|h| {
754 let mut m = IndexMap::new();
755 for (k, v) in pairs {
756 m.insert(k, h.new_str(v));
757 }
758 h.new_object(m)
759 }))
760 }
761 "flushHeaders" => Ok(Value::Undef),
762 _ => Err(crate::host::type_error(&format!(
763 "res.{method} is not a function"
764 ))),
765 }
766}
767
768fn finish_response(res: &Value, resid: u64) -> Result<(), String> {
770 let js_status = with_host(|h| match h.get(res) {
772 Some(JsObj::Object(p)) => p.get("statusCode").map(|v| h.to_number(v) as u16),
773 _ => None,
774 });
775 let st = RESPONSES.with(|r| r.borrow_mut().remove(&resid));
776 let Some(mut st) = st else { return Ok(()) };
777 if let Some(s) = js_status {
778 st.status = s;
779 }
780 let payload = serialize_response(&mut st);
781 super::net::socket_write_id(st.sock_id, &payload);
782 super::events::instance_call(res, "emit", vec![with_host(|h| h.new_str("finish"))])?;
783 Ok(())
784}
785
786fn serialize_response(st: &mut ResState) -> Vec<u8> {
790 let reason = st
791 .message
792 .clone()
793 .unwrap_or_else(|| status_text(st.status).to_string());
794 let mut out = format!("HTTP/1.1 {} {}\r\n", st.status, reason).into_bytes();
795
796 let has = |name: &str| st.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
797 let chunked = st.headers.iter().any(|(k, v)| {
798 k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
799 });
800
801 for (k, v) in &st.headers {
802 out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
803 }
804 if !chunked && !has("content-length") {
805 out.extend_from_slice(format!("Content-Length: {}\r\n", st.body.len()).as_bytes());
806 }
807 if !has("connection") {
808 out.extend_from_slice(b"Connection: keep-alive\r\n");
809 }
810 out.extend_from_slice(b"\r\n");
811 if st.head {
824 } else if chunked {
827 if !st.body.is_empty() {
828 out.extend_from_slice(format!("{:x}\r\n", st.body.len()).as_bytes());
829 out.extend_from_slice(&st.body);
830 out.extend_from_slice(b"\r\n");
831 }
832 out.extend_from_slice(b"0\r\n\r\n");
833 } else {
834 out.extend_from_slice(&st.body);
835 }
836 out
837}
838
839fn upsert_header(headers: &mut Vec<(String, String)>, name: &str, value: String) {
843 if let Some(slot) = headers
844 .iter_mut()
845 .find(|(k, _)| k.eq_ignore_ascii_case(name))
846 {
847 slot.1 = value;
848 } else {
849 headers.push((name.to_string(), value));
850 }
851}
852
853fn object_pairs(obj: &Value) -> Vec<(String, String)> {
856 with_host(|h| match h.get(obj) {
857 Some(JsObj::Object(p)) => p
858 .iter()
859 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
860 .map(|(k, v)| (k.clone(), h.str_of(v)))
861 .collect(),
862 _ => Vec::new(),
863 })
864}
865
866fn set_res_prop(res: &Value, key: &str, val: Value) {
868 with_host(|h| {
869 if let Some(JsObj::Object(p)) = h.get_mut(res) {
870 p.insert(key.to_string(), val);
871 }
872 });
873}
874
875fn value_bytes(v: Option<&Value>) -> Vec<u8> {
877 let Some(v) = v else { return Vec::new() };
878 let is_buffer =
879 with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
880 if is_buffer {
881 return with_host(|h| match h.get(v) {
882 Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
883 Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
884 _ => Vec::new(),
885 },
886 _ => Vec::new(),
887 });
888 }
889 with_host(|h| h.str_of(v)).into_bytes()
890}
891
892struct ClientReq {
902 host: String,
903 port: u16,
904 method: String,
905 path: String,
906 headers: Vec<(String, String)>,
907 body: Vec<u8>,
908 request: Value,
910 sent: bool,
911}
912
913thread_local! {
914 static CLIENT_REQS: std::cell::RefCell<HashMap<u64, ClientReq>> =
915 std::cell::RefCell::new(HashMap::new());
916 static NEXT_REQID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
917}
918
919fn next_reqid() -> u64 {
920 NEXT_REQID.with(|c| {
921 let id = c.get();
922 c.set(id + 1);
923 id
924 })
925}
926
927fn get_prop(recv: &Value, key: &str) -> Option<Value> {
928 with_host(|h| match h.get(recv) {
929 Some(JsObj::Object(p)) => p.get(key).cloned(),
930 _ => None,
931 })
932}
933
934fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
935 get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
936}
937
938pub fn request(args: &[Value], is_get: bool) -> Result<Value, String> {
941 let mut host = "localhost".to_string();
942 let mut port: u16 = 80;
943 let mut path = "/".to_string();
944 let mut method = "GET".to_string();
945 let mut headers: Vec<(String, String)> = Vec::new();
946 let mut cb: Option<Value> = None;
947
948 for a in args {
949 if with_host(|h| crate::host::is_callable(h, a)) {
950 cb = Some(a.clone());
951 } else if with_host(|h| h.as_str(a)).is_some() {
952 let url = with_host(|h| h.str_of(a));
953 parse_url(&url, &mut host, &mut port, &mut path);
954 } else if matches!(a, Value::Obj(_)) {
955 for key in ["hostname", "host"] {
956 if let Some(v) = get_prop(a, key).filter(|v| with_host(|h| h.as_str(v)).is_some()) {
957 host = with_host(|h| h.str_of(&v));
958 }
959 }
960 if let Some(v) = get_prop(a, "port") {
961 let n = with_host(|h| h.to_number(&v));
962 if !n.is_nan() {
963 port = n as u16;
964 }
965 }
966 if let Some(v) = get_prop(a, "path").filter(|v| with_host(|h| h.as_str(v)).is_some()) {
967 path = with_host(|h| h.str_of(&v));
968 }
969 if let Some(v) = get_prop(a, "method").filter(|v| with_host(|h| h.as_str(v)).is_some())
970 {
971 method = with_host(|h| h.str_of(&v));
972 }
973 if let Some(hv) = get_prop(a, "headers").filter(|v| matches!(v, Value::Obj(_))) {
974 for (k, val) in object_pairs(&hv) {
975 headers.push((k, val));
976 }
977 }
978 }
979 }
980 if is_get {
981 method = "GET".to_string();
982 }
983
984 let reqid = next_reqid();
985 let mut extra = IndexMap::new();
986 extra.insert("@@reqid".into(), Value::Float(reqid as f64));
987 extra.insert("method".into(), with_host(|h| h.new_str(method.clone())));
988 extra.insert("path".into(), with_host(|h| h.new_str(path.clone())));
989 let request = super::net::new_emitter_object("ClientRequest", extra);
990 if let Some(cb) = cb {
992 super::events::instance_call(
993 &request,
994 "on",
995 vec![with_host(|h| h.new_str("response")), cb],
996 )?;
997 }
998 CLIENT_REQS.with(|c| {
999 c.borrow_mut().insert(
1000 reqid,
1001 ClientReq {
1002 host,
1003 port,
1004 method,
1005 path,
1006 headers,
1007 body: Vec::new(),
1008 request: request.clone(),
1009 sent: false,
1010 },
1011 );
1012 });
1013 if is_get {
1014 dispatch_request(reqid)?;
1015 }
1016 Ok(request)
1017}
1018
1019fn parse_url(url: &str, host: &mut String, port: &mut u16, path: &mut String) {
1021 let rest = url.strip_prefix("http://").unwrap_or(url);
1022 let (authority, p) = match rest.find('/') {
1023 Some(i) => (&rest[..i], &rest[i..]),
1024 None => (rest, "/"),
1025 };
1026 *path = if p.is_empty() {
1027 "/".to_string()
1028 } else {
1029 p.to_string()
1030 };
1031 if let Some((h, port_str)) = authority.rsplit_once(':') {
1032 *host = h.to_string();
1033 if let Ok(n) = port_str.parse::<u16>() {
1034 *port = n;
1035 }
1036 } else {
1037 *host = authority.to_string();
1038 *port = 80;
1039 }
1040}
1041
1042fn client_request_call(req: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1043 let reqid = u64_prop(req, "@@reqid");
1044 match method {
1045 "write" => {
1046 if let Some(id) = reqid {
1047 let bytes = value_bytes(args.first());
1048 CLIENT_REQS.with(|c| {
1049 if let Some(r) = c.borrow_mut().get_mut(&id) {
1050 r.body.extend_from_slice(&bytes);
1051 }
1052 });
1053 }
1054 Ok(Value::Bool(true))
1055 }
1056 "end" => {
1057 if let Some(id) = reqid {
1058 if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
1059 let bytes = value_bytes(Some(chunk));
1060 CLIENT_REQS.with(|c| {
1061 if let Some(r) = c.borrow_mut().get_mut(&id) {
1062 r.body.extend_from_slice(&bytes);
1063 }
1064 });
1065 }
1066 dispatch_request(id)?;
1067 }
1068 Ok(req.clone())
1069 }
1070 "setHeader" => {
1071 if let Some(id) = reqid {
1072 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1073 let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
1074 CLIENT_REQS.with(|c| {
1075 if let Some(r) = c.borrow_mut().get_mut(&id) {
1076 upsert_header(&mut r.headers, &k, v);
1077 }
1078 });
1079 }
1080 Ok(Value::Undef)
1081 }
1082 "getHeader" => {
1083 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
1084 .to_ascii_lowercase();
1085 let val = reqid.and_then(|id| {
1086 CLIENT_REQS.with(|c| {
1087 c.borrow().get(&id).and_then(|r| {
1088 r.headers
1089 .iter()
1090 .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
1091 .map(|(_, v)| v.clone())
1092 })
1093 })
1094 });
1095 Ok(val
1096 .map(|v| with_host(|h| h.new_str(v)))
1097 .unwrap_or(Value::Undef))
1098 }
1099 "removeHeader" => {
1100 if let Some(id) = reqid {
1101 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1102 CLIENT_REQS.with(|c| {
1103 if let Some(r) = c.borrow_mut().get_mut(&id) {
1104 r.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
1105 }
1106 });
1107 }
1108 Ok(Value::Undef)
1109 }
1110 "abort" | "destroy" | "setTimeout" | "flushHeaders" => Ok(req.clone()),
1111 _ => Err(crate::host::type_error(&format!(
1112 "req.{method} is not a function"
1113 ))),
1114 }
1115}
1116
1117fn dispatch_request(reqid: u64) -> Result<(), String> {
1120 let sent = CLIENT_REQS.with(|c| c.borrow().get(&reqid).map(|r| r.sent).unwrap_or(true));
1121 if sent {
1122 return Ok(());
1123 }
1124 CLIENT_REQS.with(|c| {
1125 if let Some(r) = c.borrow_mut().get_mut(&reqid) {
1126 r.sent = true;
1127 }
1128 });
1129
1130 let (host, port, method, path, headers, body) = CLIENT_REQS.with(|c| {
1131 let b = c.borrow();
1132 let r = b.get(&reqid).unwrap();
1133 (
1134 r.host.clone(),
1135 r.port,
1136 r.method.clone(),
1137 r.path.clone(),
1138 r.headers.clone(),
1139 r.body.clone(),
1140 )
1141 });
1142
1143 let io_tx = with_host(|h| h.io_sender());
1144 with_host(|h| h.incr_handle());
1145
1146 let mut has_host = false;
1148 let mut has_len = false;
1149 let mut header_block = String::new();
1150 for (k, v) in &headers {
1151 if k.eq_ignore_ascii_case("host") {
1152 has_host = true;
1153 }
1154 if k.eq_ignore_ascii_case("content-length") {
1155 has_len = true;
1156 }
1157 if k.eq_ignore_ascii_case("connection") {
1158 continue;
1159 }
1160 header_block.push_str(&format!("{k}: {v}\r\n"));
1161 }
1162 let host_header = if port == 80 {
1163 host.clone()
1164 } else {
1165 format!("{host}:{port}")
1166 };
1167 let mut request_bytes = format!("{method} {path} HTTP/1.1\r\n");
1168 if !has_host {
1169 request_bytes.push_str(&format!("Host: {host_header}\r\n"));
1170 }
1171 request_bytes.push_str(&header_block);
1172 if !has_len && !body.is_empty() {
1173 request_bytes.push_str(&format!("Content-Length: {}\r\n", body.len()));
1174 }
1175 request_bytes.push_str("Connection: close\r\n\r\n");
1176 let mut wire = request_bytes.into_bytes();
1177 wire.extend_from_slice(&body);
1178
1179 std::thread::spawn(move || match exchange(&host, port, &wire) {
1180 Ok(raw) => {
1181 let _ = io_tx.send(Box::new(move || deliver_response(reqid, raw)));
1182 }
1183 Err(msg) => {
1184 let _ = io_tx.send(Box::new(move || deliver_error(reqid, msg)));
1185 }
1186 });
1187 Ok(())
1188}
1189
1190pub(crate) fn exchange(host: &str, port: u16, request: &[u8]) -> Result<Vec<u8>, String> {
1203 let mut stream = TcpStream::connect((host, port))
1204 .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
1205 stream
1206 .write_all(request)
1207 .map_err(|e| format!("Error: http write: {e}"))?;
1208 stream
1209 .flush()
1210 .map_err(|e| format!("Error: http flush: {e}"))?;
1211 let head_request = request
1212 .split(|b| *b == b' ')
1213 .next()
1214 .is_some_and(|m| m.eq_ignore_ascii_case(b"HEAD"));
1215 let mut raw = Vec::new();
1216 let mut buf = [0u8; 16384];
1217 loop {
1218 if response_is_complete(&raw, head_request) {
1219 break;
1220 }
1221 match stream.read(&mut buf) {
1222 Ok(0) => break,
1223 Ok(n) => raw.extend_from_slice(&buf[..n]),
1224 Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1225 Err(e) => {
1226 if raw.is_empty() {
1227 return Err(format!("Error: http read: {e}"));
1228 }
1229 break;
1230 }
1231 }
1232 }
1233 Ok(raw)
1234}
1235
1236pub(crate) fn response_is_complete(raw: &[u8], head_request: bool) -> bool {
1242 let Some(head_end) = find_subslice(raw, b"\r\n\r\n") else {
1243 return false;
1244 };
1245 let head = String::from_utf8_lossy(&raw[..head_end]);
1246 let mut lines = head.split("\r\n");
1247 let status = lines
1248 .next()
1249 .and_then(|l| l.split(' ').nth(1).and_then(|s| s.parse::<u16>().ok()))
1250 .unwrap_or(0);
1251 if (100..200).contains(&status) {
1254 return false;
1255 }
1256 if head_request || status == 204 || status == 304 {
1258 return true;
1259 }
1260 let mut chunked = false;
1261 let mut content_length: Option<usize> = None;
1262 for line in lines {
1263 let Some((k, v)) = line.split_once(':') else {
1264 continue;
1265 };
1266 let name = k.trim().to_ascii_lowercase();
1267 let value = v.trim();
1268 if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1269 chunked = true;
1270 } else if name == "content-length" {
1271 content_length = value.parse().ok();
1272 }
1273 }
1274 let body = &raw[head_end + 4..];
1275 if chunked {
1277 return chunked_body_is_terminated(body);
1278 }
1279 match content_length {
1280 Some(n) => body.len() >= n,
1281 None => false,
1282 }
1283}
1284
1285fn chunked_body_is_terminated(mut data: &[u8]) -> bool {
1288 loop {
1289 let Some(nl) = find_subslice(data, b"\r\n") else {
1290 return false;
1291 };
1292 let size_line = String::from_utf8_lossy(&data[..nl]);
1293 let Ok(size) = usize::from_str_radix(size_line.split(';').next().unwrap_or("").trim(), 16)
1294 else {
1295 return false;
1296 };
1297 if size == 0 {
1298 return find_subslice(&data[nl + 2..], b"\r\n").is_some();
1300 }
1301 let consumed = nl + 2 + size + 2;
1303 if data.len() < consumed {
1304 return false;
1305 }
1306 data = &data[consumed..];
1307 }
1308}
1309
1310fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
1313 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1314 with_host(|h| h.decr_handle());
1315 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1316 let Some(entry) = entry else { return Ok(()) };
1317
1318 let (status, message, http_version, headers, body) = parse_raw_response(&raw);
1319
1320 let headers_obj = with_host(|h| {
1321 let mut m = IndexMap::new();
1322 for (k, v) in &headers {
1323 m.insert(k.clone(), h.new_str(v.clone()));
1324 }
1325 h.new_object(m)
1326 });
1327 let mut extra = IndexMap::new();
1328 extra.insert("statusCode".into(), Value::Float(status as f64));
1329 extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
1330 extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
1331 extra.insert("headers".into(), headers_obj);
1332 let res = super::net::new_emitter_object("IncomingMessage", extra);
1333
1334 super::events::instance_call(
1335 &entry.request,
1336 "emit",
1337 vec![with_host(|h| h.new_str("response")), res.clone()],
1338 )?;
1339 if !body.is_empty() {
1340 let chunk = super::buffer::from_bytes(&body);
1341 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
1342 }
1343 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
1344 Ok(())
1345}
1346
1347fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
1349 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1350 with_host(|h| h.decr_handle());
1351 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1352 if let Some(entry) = entry {
1353 let err = with_host(|h| {
1354 let mut m = IndexMap::new();
1355 m.insert("message".into(), h.new_str(msg.clone()));
1356 h.new_object(m)
1357 });
1358 super::events::instance_call(
1359 &entry.request,
1360 "emit",
1361 vec![with_host(|h| h.new_str("error")), err],
1362 )?;
1363 }
1364 Ok(())
1365}
1366
1367pub(crate) fn parse_raw_response(
1370 raw: &[u8],
1371) -> (u16, String, String, Vec<(String, String)>, Vec<u8>) {
1372 let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
1373 let head = String::from_utf8_lossy(&raw[..head_end]);
1374 let body_start = (head_end + 4).min(raw.len());
1375 let mut lines = head.split("\r\n");
1376 let status_line = lines.next().unwrap_or("");
1377 let mut sp = status_line.splitn(3, ' ');
1378 let version = sp
1379 .next()
1380 .unwrap_or("HTTP/1.1")
1381 .strip_prefix("HTTP/")
1382 .unwrap_or("1.1")
1383 .to_string();
1384 let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
1385 let message = sp.next().unwrap_or("").to_string();
1386
1387 let mut headers: Vec<(String, String)> = Vec::new();
1388 let mut chunked = false;
1389 for line in lines {
1390 if line.is_empty() {
1391 continue;
1392 }
1393 if let Some((k, v)) = line.split_once(':') {
1394 let name = k.trim().to_ascii_lowercase();
1395 let value = v.trim().to_string();
1396 if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1397 chunked = true;
1398 }
1399 headers.push((name, value));
1400 }
1401 }
1402 let raw_body = &raw[body_start..];
1403 let body = if chunked {
1404 decode_chunked(raw_body)
1405 } else {
1406 raw_body.to_vec()
1407 };
1408 (status, message, version, headers, body)
1409}
1410
1411fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1414 let mut out = Vec::new();
1415 while let Some(nl) = find_subslice(data, b"\r\n") {
1416 let size_line = String::from_utf8_lossy(&data[..nl]);
1417 let size_hex = size_line.split(';').next().unwrap_or("").trim();
1418 let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1419 if size == 0 {
1420 break;
1421 }
1422 let chunk_start = nl + 2;
1423 let chunk_end = (chunk_start + size).min(data.len());
1424 out.extend_from_slice(&data[chunk_start..chunk_end]);
1425 let next = chunk_end + 2;
1426 if next >= data.len() {
1427 break;
1428 }
1429 data = &data[next..];
1430 }
1431 out
1432}
1433
1434fn status_text(code: u16) -> &'static str {
1436 match code {
1437 200 => "OK",
1438 201 => "Created",
1439 202 => "Accepted",
1440 204 => "No Content",
1441 301 => "Moved Permanently",
1442 302 => "Found",
1443 304 => "Not Modified",
1444 400 => "Bad Request",
1445 401 => "Unauthorized",
1446 403 => "Forbidden",
1447 404 => "Not Found",
1448 405 => "Method Not Allowed",
1449 409 => "Conflict",
1450 500 => "Internal Server Error",
1451 502 => "Bad Gateway",
1452 503 => "Service Unavailable",
1453 _ => "OK",
1454 }
1455}
1456
1457#[cfg(test)]
1458mod framing_tests {
1459 use super::{response_is_complete, serialize_response, ResState};
1460
1461 #[test]
1464 fn a_keep_alive_content_length_response_is_complete_at_its_last_body_byte() {
1465 let head = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\
1466 Content-Length: 12\r\nConnection: keep-alive\r\n\r\n";
1467 let mut raw = head.to_vec();
1468 assert!(!response_is_complete(&raw, false), "no body yet");
1469 raw.extend_from_slice(b"hello GET /");
1470 assert!(!response_is_complete(&raw, false), "11 of 12 bytes");
1471 raw.push(b'p');
1472 assert!(response_is_complete(&raw, false), "12 of 12 bytes");
1473 }
1474
1475 #[test]
1481 fn a_chunked_response_this_server_wrote_is_complete_to_this_client() {
1482 let mut st = ResState {
1483 sock_id: 0,
1484 head: false,
1485 status: 200,
1486 message: None,
1487 headers: vec![("Transfer-Encoding".into(), "chunked".into())],
1488 body: b"onetwo".to_vec(),
1489 };
1490 let wire = serialize_response(&mut st);
1491 assert!(
1492 wire.ends_with(b"\r\n\r\n6\r\nonetwo\r\n0\r\n\r\n"),
1493 "unexpected framing: {}",
1494 String::from_utf8_lossy(&wire)
1495 );
1496 assert!(response_is_complete(&wire, false));
1497 for cut in 1..wire.len() {
1500 assert!(
1501 !response_is_complete(&wire[..cut], false),
1502 "a {cut}-byte prefix was reported complete"
1503 );
1504 }
1505 }
1506
1507 #[test]
1509 fn an_empty_chunked_response_is_just_the_zero_chunk() {
1510 let mut st = ResState {
1511 sock_id: 0,
1512 head: false,
1513 status: 200,
1514 message: None,
1515 headers: vec![("Transfer-Encoding".into(), "chunked".into())],
1516 body: Vec::new(),
1517 };
1518 let wire = serialize_response(&mut st);
1519 assert!(
1520 wire.ends_with(b"\r\n\r\n0\r\n\r\n"),
1521 "{}",
1522 String::from_utf8_lossy(&wire)
1523 );
1524 assert!(response_is_complete(&wire, false));
1525 }
1526
1527 #[test]
1529 fn a_head_response_advertises_the_get_length_and_sends_no_body() {
1530 let mk = |head: bool| {
1531 let mut st = ResState {
1532 sock_id: 0,
1533 head,
1534 status: 200,
1535 message: None,
1536 headers: vec![("Content-Type".into(), "text/plain".into())],
1537 body: b"body-here".to_vec(),
1538 };
1539 serialize_response(&mut st)
1540 };
1541 let head = mk(true);
1542 let get = mk(false);
1543 let text = String::from_utf8_lossy(&head).into_owned();
1544 assert!(text.contains("Content-Length: 9"), "{text}");
1545 assert!(
1546 text.ends_with("\r\n\r\n"),
1547 "HEAD must stop at the blank line: {text}"
1548 );
1549 assert!(String::from_utf8_lossy(&get).ends_with("body-here"));
1550 let split = |v: &[u8]| {
1552 String::from_utf8_lossy(v)
1553 .split("\r\n\r\n")
1554 .next()
1555 .unwrap()
1556 .to_string()
1557 };
1558 assert_eq!(split(&head), split(&get));
1559 }
1560
1561 #[test]
1562 fn a_partial_header_block_is_never_complete() {
1563 assert!(!response_is_complete(b"", false));
1564 assert!(!response_is_complete(b"HTTP/1.1 200 OK\r\n", false));
1565 assert!(!response_is_complete(
1566 b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n",
1567 false
1568 ));
1569 }
1570
1571 #[test]
1574 fn an_unframed_response_is_never_reported_complete() {
1575 let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nbody bytes";
1576 assert!(!response_is_complete(raw, false));
1577 }
1578
1579 #[test]
1580 fn a_chunked_body_completes_only_at_its_zero_chunk() {
1581 let head = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n";
1582 let mut raw = head.to_vec();
1583 assert!(!response_is_complete(&raw, false));
1584 raw.extend_from_slice(b"5\r\nhello\r\n");
1585 assert!(
1586 !response_is_complete(&raw, false),
1587 "one chunk, no terminator"
1588 );
1589 raw.extend_from_slice(b"0\r\n");
1590 assert!(
1591 !response_is_complete(&raw, false),
1592 "trailer CRLF still missing"
1593 );
1594 raw.extend_from_slice(b"\r\n");
1595 assert!(response_is_complete(&raw, false));
1596 }
1597
1598 #[test]
1601 fn chunk_extensions_parse_and_chunked_outranks_content_length() {
1602 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 99\r\nTransfer-Encoding: chunked\r\n\r\n\
1603 3;name=v\r\nabc\r\n0\r\n\r\n";
1604 assert!(response_is_complete(raw, false));
1605 let short =
1606 b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nTransfer-Encoding: chunked\r\n\r\nabc";
1607 assert!(
1608 !response_is_complete(short, false),
1609 "Content-Length must not settle a chunked response"
1610 );
1611 }
1612
1613 #[test]
1616 fn bodiless_statuses_and_head_finish_at_the_header_block() {
1617 assert!(response_is_complete(
1618 b"HTTP/1.1 204 No Content\r\nContent-Length: 7\r\n\r\n",
1619 false
1620 ));
1621 assert!(response_is_complete(
1622 b"HTTP/1.1 304 Not Modified\r\nContent-Length: 7\r\n\r\n",
1623 false
1624 ));
1625 assert!(response_is_complete(
1626 b"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\n",
1627 true
1628 ));
1629 assert!(
1630 !response_is_complete(b"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\n", false),
1631 "the same response to a GET still needs its body"
1632 );
1633 }
1634
1635 #[test]
1637 fn an_interim_response_does_not_end_the_read() {
1638 assert!(!response_is_complete(
1639 b"HTTP/1.1 100 Continue\r\n\r\n",
1640 false
1641 ));
1642 assert!(!response_is_complete(
1643 b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n",
1644 false
1645 ));
1646 }
1647}