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 raw_headers: Vec<(String, String)>,
441 body: Vec<u8>,
442}
443
444fn parse_request(buf: &[u8]) -> Option<(ParsedReq, usize)> {
447 let head_end = find_subslice(buf, b"\r\n\r\n")?;
449 let head = &buf[..head_end];
450 let body_start = head_end + 4;
451
452 let head_str = String::from_utf8_lossy(head);
453 let mut lines = head_str.split("\r\n");
454 let request_line = lines.next()?;
455 let mut parts = request_line.split(' ');
456 let method = parts.next()?.to_string();
457 let url = parts.next()?.to_string();
458 let version = parts.next().unwrap_or("HTTP/1.1");
459 let http_version = version.strip_prefix("HTTP/").unwrap_or("1.1").to_string();
460
461 let mut headers: Vec<(String, String)> = Vec::new();
462 let mut raw_headers: Vec<(String, String)> = Vec::new();
463 let mut content_length = 0usize;
464 for line in lines {
465 if line.is_empty() {
466 continue;
467 }
468 if let Some((k, v)) = line.split_once(':') {
469 let name = k.trim().to_ascii_lowercase();
470 let value = v.trim().to_string();
471 if name == "content-length" {
472 content_length = value.parse().unwrap_or(0);
473 }
474 raw_headers.push((k.trim().to_string(), value.clone()));
475 headers.push((name, value));
476 }
477 }
478
479 if buf.len() < body_start + content_length {
481 return None;
482 }
483 let body = buf[body_start..body_start + content_length].to_vec();
484 Some((
485 ParsedReq {
486 method,
487 url,
488 http_version,
489 headers,
490 raw_headers,
491 body,
492 },
493 body_start + content_length,
494 ))
495}
496
497fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
498 haystack.windows(needle.len()).position(|w| w == needle)
499}
500
501fn flat_headers(pairs: &[(String, String)]) -> Value {
506 with_host(|h| {
507 let flat: Vec<Value> = pairs
508 .iter()
509 .flat_map(|(k, v)| [h.new_str(k.clone()), h.new_str(v.clone())])
510 .collect();
511 h.new_array(flat)
512 })
513}
514
515fn build_incoming(req: &ParsedReq) -> Value {
516 let headers_obj = with_host(|h| {
517 let mut m = IndexMap::new();
518 for (k, v) in &req.headers {
519 m.insert(k.clone(), h.new_str(v.clone()));
520 }
521 h.new_object(m)
522 });
523 let mut extra = IndexMap::new();
524 extra.insert(
525 "method".into(),
526 with_host(|h| h.new_str(req.method.clone())),
527 );
528 extra.insert("url".into(), with_host(|h| h.new_str(req.url.clone())));
529 extra.insert(
530 "httpVersion".into(),
531 with_host(|h| h.new_str(req.http_version.clone())),
532 );
533 extra.insert("headers".into(), headers_obj);
534 extra.insert("rawHeaders".into(), flat_headers(&req.raw_headers));
535 super::net::new_emitter_object("IncomingMessage", extra)
536}
537
538fn incoming_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
545 match method {
546 "pause" | "resume" | "unpipe" | "destroy" => Ok(recv.clone()),
550 "isPaused" => Ok(Value::Bool(false)),
551 "setEncoding" => {
553 let enc = super::arg_str(&args, 0);
554 with_host(|h| {
555 let v = h.new_str(enc);
556 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
557 p.insert("@@encoding".into(), v);
558 }
559 });
560 Ok(recv.clone())
561 }
562 _ => Err(crate::host::type_error(&format!(
563 "req.{method} is not a function"
564 ))),
565 }
566}
567
568fn build_response(sock_id: u64, head: bool) -> Value {
571 let resid = next_resid();
572 RESPONSES.with(|r| {
573 r.borrow_mut().insert(
574 resid,
575 ResState {
576 sock_id,
577 head,
578 status: 200,
579 message: None,
580 headers: Vec::new(),
581 body: Vec::new(),
582 },
583 );
584 });
585 let mut extra = IndexMap::new();
586 extra.insert("@@resid".into(), Value::Float(resid as f64));
587 extra.insert("statusCode".into(), Value::Float(200.0));
588 super::net::new_emitter_object("ServerResponse", extra)
589}
590
591fn resid_of(res: &Value) -> Option<u64> {
592 with_host(|h| match h.get(res) {
593 Some(JsObj::Object(p)) => p.get("@@resid").map(|v| h.to_number(v) as u64),
594 _ => None,
595 })
596}
597
598pub fn instance_call(
601 tag: &str,
602 recv: &Value,
603 method: &str,
604 args: Vec<Value>,
605) -> Result<Value, String> {
606 if super::events::METHODS.contains(&method) {
612 return super::events::instance_call(recv, method, args);
613 }
614 match tag {
615 "IncomingMessage" => incoming_call(recv, method, args),
616 "ServerResponse" => response_call(recv, method, args),
617 "ClientRequest" => client_request_call(recv, method, args),
618 "Agent" => match method {
620 "destroy" => Ok(Value::Undef),
621 "getName" => Ok(with_host(|h| h.new_str("localhost:80:"))),
622 _ => Err(crate::host::type_error(&format!(
623 "agent.{method} is not a function"
624 ))),
625 },
626 _ => Err(crate::host::type_error(&format!(
627 "{method} is not a function"
628 ))),
629 }
630}
631
632fn response_call(res: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
633 let Some(resid) = resid_of(res) else {
634 return Err(crate::host::type_error("invalid ServerResponse"));
635 };
636 match method {
637 "writeHead" => {
638 let status =
639 with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u16;
640 let mut message: Option<String> = None;
642 let mut headers_arg: Option<Value> = None;
643 if let Some(a) = args.get(1) {
644 if with_host(|h| h.as_str(a)).is_some() {
645 message = Some(with_host(|h| h.str_of(a)));
646 } else if !matches!(a, Value::Undef) {
647 headers_arg = Some(a.clone());
648 }
649 }
650 if let Some(a) = args.get(2) {
651 if !matches!(a, Value::Undef) {
652 headers_arg = Some(a.clone());
653 }
654 }
655 let header_pairs = headers_arg.map(|h| object_pairs(&h)).unwrap_or_default();
656 RESPONSES.with(|r| {
657 if let Some(st) = r.borrow_mut().get_mut(&resid) {
658 st.status = status;
659 st.message = message;
660 for (k, v) in header_pairs {
661 upsert_header(&mut st.headers, &k, v);
662 }
663 }
664 });
665 set_res_prop(res, "statusCode", Value::Float(status as f64));
667 Ok(res.clone())
668 }
669 "setHeader" => {
670 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
671 let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
672 RESPONSES.with(|r| {
673 if let Some(st) = r.borrow_mut().get_mut(&resid) {
674 upsert_header(&mut st.headers, &k, v);
675 }
676 });
677 Ok(Value::Undef)
678 }
679 "getHeader" => {
680 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
681 .to_ascii_lowercase();
682 let val = RESPONSES.with(|r| {
683 r.borrow().get(&resid).and_then(|st| {
684 st.headers
685 .iter()
686 .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
687 .map(|(_, v)| v.clone())
688 })
689 });
690 Ok(val
691 .map(|v| with_host(|h| h.new_str(v)))
692 .unwrap_or(Value::Undef))
693 }
694 "removeHeader" => {
695 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
696 RESPONSES.with(|r| {
697 if let Some(st) = r.borrow_mut().get_mut(&resid) {
698 st.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
699 }
700 });
701 Ok(Value::Undef)
702 }
703 "write" => {
704 let bytes = value_bytes(args.first());
705 RESPONSES.with(|r| {
706 if let Some(st) = r.borrow_mut().get_mut(&resid) {
707 st.body.extend_from_slice(&bytes);
708 }
709 });
710 Ok(Value::Bool(true))
711 }
712 "end" => {
713 if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
714 let bytes = value_bytes(Some(chunk));
715 RESPONSES.with(|r| {
716 if let Some(st) = r.borrow_mut().get_mut(&resid) {
717 st.body.extend_from_slice(&bytes);
718 }
719 });
720 }
721 finish_response(res, resid)?;
722 Ok(res.clone())
723 }
724 "hasHeader" => {
736 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
737 let found = RESPONSES.with(|r| {
738 r.borrow()
739 .get(&resid)
740 .is_some_and(|st| st.headers.iter().any(|(hk, _)| hk.eq_ignore_ascii_case(&k)))
741 });
742 Ok(Value::Bool(found))
743 }
744 "getHeaderNames" => {
745 let names = RESPONSES.with(|r| {
746 r.borrow()
747 .get(&resid)
748 .map(|st| {
749 st.headers
750 .iter()
751 .map(|(k, _)| k.to_ascii_lowercase())
752 .collect::<Vec<_>>()
753 })
754 .unwrap_or_default()
755 });
756 Ok(with_host(|h| {
757 let items = names.into_iter().map(|n| h.new_str(n)).collect::<Vec<_>>();
758 h.new_array(items)
759 }))
760 }
761 "getHeaders" => {
762 let pairs = RESPONSES.with(|r| {
763 r.borrow()
764 .get(&resid)
765 .map(|st| {
766 st.headers
767 .iter()
768 .map(|(k, v)| (k.to_ascii_lowercase(), v.clone()))
769 .collect::<Vec<_>>()
770 })
771 .unwrap_or_default()
772 });
773 Ok(with_host(|h| {
774 let mut m = IndexMap::new();
775 for (k, v) in pairs {
776 m.insert(k, h.new_str(v));
777 }
778 h.new_object(m)
779 }))
780 }
781 "flushHeaders" => Ok(Value::Undef),
782 _ => Err(crate::host::type_error(&format!(
783 "res.{method} is not a function"
784 ))),
785 }
786}
787
788fn finish_response(res: &Value, resid: u64) -> Result<(), String> {
790 let (js_status, js_message) = with_host(|h| match h.get(res) {
795 Some(JsObj::Object(p)) => (
796 p.get("statusCode").map(|v| h.to_number(v) as u16),
797 p.get("statusMessage")
798 .filter(|v| !matches!(v, Value::Undef))
799 .map(|v| h.str_of(v)),
800 ),
801 _ => (None, None),
802 });
803 let st = RESPONSES.with(|r| r.borrow_mut().remove(&resid));
804 let Some(mut st) = st else { return Ok(()) };
805 if let Some(s) = js_status {
806 st.status = s;
807 }
808 if let Some(m) = js_message {
809 st.message = Some(m);
810 }
811 let payload = serialize_response(&mut st);
812 super::net::socket_write_id(st.sock_id, &payload);
813 super::events::instance_call(res, "emit", vec![with_host(|h| h.new_str("finish"))])?;
814 Ok(())
815}
816
817fn serialize_response(st: &mut ResState) -> Vec<u8> {
821 let reason = st
822 .message
823 .clone()
824 .unwrap_or_else(|| status_text(st.status).to_string());
825 let mut out = format!("HTTP/1.1 {} {}\r\n", st.status, reason).into_bytes();
826
827 let has = |name: &str| st.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
828 let chunked = st.headers.iter().any(|(k, v)| {
829 k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
830 });
831
832 for (k, v) in &st.headers {
833 out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
834 }
835 if !chunked && !has("content-length") {
836 out.extend_from_slice(format!("Content-Length: {}\r\n", st.body.len()).as_bytes());
837 }
838 if !has("connection") {
839 out.extend_from_slice(b"Connection: keep-alive\r\n");
840 }
841 out.extend_from_slice(b"\r\n");
842 if st.head {
855 } else if chunked {
858 if !st.body.is_empty() {
859 out.extend_from_slice(format!("{:x}\r\n", st.body.len()).as_bytes());
860 out.extend_from_slice(&st.body);
861 out.extend_from_slice(b"\r\n");
862 }
863 out.extend_from_slice(b"0\r\n\r\n");
864 } else {
865 out.extend_from_slice(&st.body);
866 }
867 out
868}
869
870fn upsert_header(headers: &mut Vec<(String, String)>, name: &str, value: String) {
874 if let Some(slot) = headers
875 .iter_mut()
876 .find(|(k, _)| k.eq_ignore_ascii_case(name))
877 {
878 slot.1 = value;
879 } else {
880 headers.push((name.to_string(), value));
881 }
882}
883
884fn object_pairs(obj: &Value) -> Vec<(String, String)> {
887 with_host(|h| match h.get(obj) {
888 Some(JsObj::Object(p)) => p
889 .iter()
890 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
891 .map(|(k, v)| (k.clone(), h.str_of(v)))
892 .collect(),
893 _ => Vec::new(),
894 })
895}
896
897fn set_res_prop(res: &Value, key: &str, val: Value) {
899 with_host(|h| {
900 if let Some(JsObj::Object(p)) = h.get_mut(res) {
901 p.insert(key.to_string(), val);
902 }
903 });
904}
905
906fn value_bytes(v: Option<&Value>) -> Vec<u8> {
908 let Some(v) = v else { return Vec::new() };
909 let is_buffer =
910 with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
911 if is_buffer {
912 return with_host(|h| match h.get(v) {
913 Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
914 Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
915 _ => Vec::new(),
916 },
917 _ => Vec::new(),
918 });
919 }
920 with_host(|h| h.str_of(v)).into_bytes()
921}
922
923struct ClientReq {
933 host: String,
934 port: u16,
935 method: String,
936 path: String,
937 headers: Vec<(String, String)>,
938 body: Vec<u8>,
939 request: Value,
941 sent: bool,
942}
943
944thread_local! {
945 static CLIENT_REQS: std::cell::RefCell<HashMap<u64, ClientReq>> =
946 std::cell::RefCell::new(HashMap::new());
947 static NEXT_REQID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
948}
949
950fn next_reqid() -> u64 {
951 NEXT_REQID.with(|c| {
952 let id = c.get();
953 c.set(id + 1);
954 id
955 })
956}
957
958fn get_prop(recv: &Value, key: &str) -> Option<Value> {
959 with_host(|h| match h.get(recv) {
960 Some(JsObj::Object(p)) => p.get(key).cloned(),
961 _ => None,
962 })
963}
964
965fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
966 get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
967}
968
969pub fn request(args: &[Value], is_get: bool) -> Result<Value, String> {
972 let mut host = "localhost".to_string();
973 let mut port: u16 = 80;
974 let mut path = "/".to_string();
975 let mut method = "GET".to_string();
976 let mut headers: Vec<(String, String)> = Vec::new();
977 let mut cb: Option<Value> = None;
978
979 for a in args {
980 if with_host(|h| crate::host::is_callable(h, a)) {
981 cb = Some(a.clone());
982 } else if with_host(|h| h.as_str(a)).is_some() {
983 let url = with_host(|h| h.str_of(a));
984 parse_url(&url, &mut host, &mut port, &mut path);
985 } else if matches!(a, Value::Obj(_)) {
986 for key in ["hostname", "host"] {
987 if let Some(v) = get_prop(a, key).filter(|v| with_host(|h| h.as_str(v)).is_some()) {
988 host = with_host(|h| h.str_of(&v));
989 }
990 }
991 if let Some(v) = get_prop(a, "port") {
992 let n = with_host(|h| h.to_number(&v));
993 if !n.is_nan() {
994 port = n as u16;
995 }
996 }
997 if let Some(v) = get_prop(a, "path").filter(|v| with_host(|h| h.as_str(v)).is_some()) {
998 path = with_host(|h| h.str_of(&v));
999 }
1000 if let Some(v) = get_prop(a, "method").filter(|v| with_host(|h| h.as_str(v)).is_some())
1001 {
1002 method = with_host(|h| h.str_of(&v));
1003 }
1004 if let Some(hv) = get_prop(a, "headers").filter(|v| matches!(v, Value::Obj(_))) {
1005 for (k, val) in object_pairs(&hv) {
1006 headers.push((k, val));
1007 }
1008 }
1009 }
1010 }
1011 if is_get {
1012 method = "GET".to_string();
1013 }
1014
1015 let reqid = next_reqid();
1016 let mut extra = IndexMap::new();
1017 extra.insert("@@reqid".into(), Value::Float(reqid as f64));
1018 extra.insert("method".into(), with_host(|h| h.new_str(method.clone())));
1019 extra.insert("path".into(), with_host(|h| h.new_str(path.clone())));
1020 let request = super::net::new_emitter_object("ClientRequest", extra);
1021 if let Some(cb) = cb {
1023 super::events::instance_call(
1024 &request,
1025 "on",
1026 vec![with_host(|h| h.new_str("response")), cb],
1027 )?;
1028 }
1029 CLIENT_REQS.with(|c| {
1030 c.borrow_mut().insert(
1031 reqid,
1032 ClientReq {
1033 host,
1034 port,
1035 method,
1036 path,
1037 headers,
1038 body: Vec::new(),
1039 request: request.clone(),
1040 sent: false,
1041 },
1042 );
1043 });
1044 if is_get {
1045 dispatch_request(reqid)?;
1046 }
1047 Ok(request)
1048}
1049
1050fn parse_url(url: &str, host: &mut String, port: &mut u16, path: &mut String) {
1052 let rest = url.strip_prefix("http://").unwrap_or(url);
1053 let (authority, p) = match rest.find('/') {
1054 Some(i) => (&rest[..i], &rest[i..]),
1055 None => (rest, "/"),
1056 };
1057 *path = if p.is_empty() {
1058 "/".to_string()
1059 } else {
1060 p.to_string()
1061 };
1062 if let Some((h, port_str)) = authority.rsplit_once(':') {
1063 *host = h.to_string();
1064 if let Ok(n) = port_str.parse::<u16>() {
1065 *port = n;
1066 }
1067 } else {
1068 *host = authority.to_string();
1069 *port = 80;
1070 }
1071}
1072
1073fn client_request_call(req: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1074 let reqid = u64_prop(req, "@@reqid");
1075 match method {
1076 "write" => {
1077 if let Some(id) = reqid {
1078 let bytes = value_bytes(args.first());
1079 CLIENT_REQS.with(|c| {
1080 if let Some(r) = c.borrow_mut().get_mut(&id) {
1081 r.body.extend_from_slice(&bytes);
1082 }
1083 });
1084 }
1085 Ok(Value::Bool(true))
1086 }
1087 "end" => {
1088 if let Some(id) = reqid {
1089 if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
1090 let bytes = value_bytes(Some(chunk));
1091 CLIENT_REQS.with(|c| {
1092 if let Some(r) = c.borrow_mut().get_mut(&id) {
1093 r.body.extend_from_slice(&bytes);
1094 }
1095 });
1096 }
1097 dispatch_request(id)?;
1098 }
1099 Ok(req.clone())
1100 }
1101 "setHeader" => {
1102 if let Some(id) = reqid {
1103 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1104 let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
1105 CLIENT_REQS.with(|c| {
1106 if let Some(r) = c.borrow_mut().get_mut(&id) {
1107 upsert_header(&mut r.headers, &k, v);
1108 }
1109 });
1110 }
1111 Ok(Value::Undef)
1112 }
1113 "getHeader" => {
1114 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
1115 .to_ascii_lowercase();
1116 let val = reqid.and_then(|id| {
1117 CLIENT_REQS.with(|c| {
1118 c.borrow().get(&id).and_then(|r| {
1119 r.headers
1120 .iter()
1121 .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
1122 .map(|(_, v)| v.clone())
1123 })
1124 })
1125 });
1126 Ok(val
1127 .map(|v| with_host(|h| h.new_str(v)))
1128 .unwrap_or(Value::Undef))
1129 }
1130 "removeHeader" => {
1131 if let Some(id) = reqid {
1132 let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1133 CLIENT_REQS.with(|c| {
1134 if let Some(r) = c.borrow_mut().get_mut(&id) {
1135 r.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
1136 }
1137 });
1138 }
1139 Ok(Value::Undef)
1140 }
1141 "abort" | "destroy" | "setTimeout" | "flushHeaders" => Ok(req.clone()),
1142 _ => Err(crate::host::type_error(&format!(
1143 "req.{method} is not a function"
1144 ))),
1145 }
1146}
1147
1148fn dispatch_request(reqid: u64) -> Result<(), String> {
1151 let sent = CLIENT_REQS.with(|c| c.borrow().get(&reqid).map(|r| r.sent).unwrap_or(true));
1152 if sent {
1153 return Ok(());
1154 }
1155 CLIENT_REQS.with(|c| {
1156 if let Some(r) = c.borrow_mut().get_mut(&reqid) {
1157 r.sent = true;
1158 }
1159 });
1160
1161 let (host, port, method, path, headers, body) = CLIENT_REQS.with(|c| {
1162 let b = c.borrow();
1163 let r = b.get(&reqid).unwrap();
1164 (
1165 r.host.clone(),
1166 r.port,
1167 r.method.clone(),
1168 r.path.clone(),
1169 r.headers.clone(),
1170 r.body.clone(),
1171 )
1172 });
1173
1174 let io_tx = with_host(|h| h.io_sender());
1175 with_host(|h| h.incr_handle());
1176
1177 let mut has_host = false;
1179 let mut has_len = false;
1180 let mut header_block = String::new();
1181 for (k, v) in &headers {
1182 if k.eq_ignore_ascii_case("host") {
1183 has_host = true;
1184 }
1185 if k.eq_ignore_ascii_case("content-length") {
1186 has_len = true;
1187 }
1188 if k.eq_ignore_ascii_case("connection") {
1189 continue;
1190 }
1191 header_block.push_str(&format!("{k}: {v}\r\n"));
1192 }
1193 let host_header = if port == 80 {
1194 host.clone()
1195 } else {
1196 format!("{host}:{port}")
1197 };
1198 let mut request_bytes = format!("{method} {path} HTTP/1.1\r\n");
1199 if !has_host {
1200 request_bytes.push_str(&format!("Host: {host_header}\r\n"));
1201 }
1202 request_bytes.push_str(&header_block);
1203 if !has_len && !body.is_empty() {
1204 request_bytes.push_str(&format!("Content-Length: {}\r\n", body.len()));
1205 }
1206 request_bytes.push_str("Connection: close\r\n\r\n");
1207 let mut wire = request_bytes.into_bytes();
1208 wire.extend_from_slice(&body);
1209
1210 std::thread::spawn(move || match exchange(&host, port, &wire) {
1211 Ok(raw) => {
1212 let _ = io_tx.send(Box::new(move || deliver_response(reqid, raw)));
1213 }
1214 Err(msg) => {
1215 let _ = io_tx.send(Box::new(move || deliver_error(reqid, msg)));
1216 }
1217 });
1218 Ok(())
1219}
1220
1221pub(crate) fn exchange(host: &str, port: u16, request: &[u8]) -> Result<Vec<u8>, String> {
1234 let mut stream = TcpStream::connect((host, port))
1235 .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
1236 stream
1237 .write_all(request)
1238 .map_err(|e| format!("Error: http write: {e}"))?;
1239 stream
1240 .flush()
1241 .map_err(|e| format!("Error: http flush: {e}"))?;
1242 let head_request = request
1243 .split(|b| *b == b' ')
1244 .next()
1245 .is_some_and(|m| m.eq_ignore_ascii_case(b"HEAD"));
1246 let mut raw = Vec::new();
1247 let mut buf = [0u8; 16384];
1248 loop {
1249 if response_is_complete(&raw, head_request) {
1250 break;
1251 }
1252 match stream.read(&mut buf) {
1253 Ok(0) => break,
1254 Ok(n) => raw.extend_from_slice(&buf[..n]),
1255 Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1256 Err(e) => {
1257 if raw.is_empty() {
1258 return Err(format!("Error: http read: {e}"));
1259 }
1260 break;
1261 }
1262 }
1263 }
1264 Ok(raw)
1265}
1266
1267pub(crate) fn response_is_complete(raw: &[u8], head_request: bool) -> bool {
1273 let Some(head_end) = find_subslice(raw, b"\r\n\r\n") else {
1274 return false;
1275 };
1276 let head = String::from_utf8_lossy(&raw[..head_end]);
1277 let mut lines = head.split("\r\n");
1278 let status = lines
1279 .next()
1280 .and_then(|l| l.split(' ').nth(1).and_then(|s| s.parse::<u16>().ok()))
1281 .unwrap_or(0);
1282 if (100..200).contains(&status) {
1285 return false;
1286 }
1287 if head_request || status == 204 || status == 304 {
1289 return true;
1290 }
1291 let mut chunked = false;
1292 let mut content_length: Option<usize> = None;
1293 for line in lines {
1294 let Some((k, v)) = line.split_once(':') else {
1295 continue;
1296 };
1297 let name = k.trim().to_ascii_lowercase();
1298 let value = v.trim();
1299 if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1300 chunked = true;
1301 } else if name == "content-length" {
1302 content_length = value.parse().ok();
1303 }
1304 }
1305 let body = &raw[head_end + 4..];
1306 if chunked {
1308 return chunked_body_is_terminated(body);
1309 }
1310 match content_length {
1311 Some(n) => body.len() >= n,
1312 None => false,
1313 }
1314}
1315
1316fn chunked_body_is_terminated(mut data: &[u8]) -> bool {
1319 loop {
1320 let Some(nl) = find_subslice(data, b"\r\n") else {
1321 return false;
1322 };
1323 let size_line = String::from_utf8_lossy(&data[..nl]);
1324 let Ok(size) = usize::from_str_radix(size_line.split(';').next().unwrap_or("").trim(), 16)
1325 else {
1326 return false;
1327 };
1328 if size == 0 {
1329 return find_subslice(&data[nl + 2..], b"\r\n").is_some();
1331 }
1332 let consumed = nl + 2 + size + 2;
1334 if data.len() < consumed {
1335 return false;
1336 }
1337 data = &data[consumed..];
1338 }
1339}
1340
1341fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
1344 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1345 with_host(|h| h.decr_handle());
1346 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1347 let Some(entry) = entry else { return Ok(()) };
1348
1349 let ParsedRes {
1350 status,
1351 message,
1352 http_version,
1353 headers,
1354 raw_headers: raw_header_pairs,
1355 body,
1356 } = parse_raw_response(&raw);
1357
1358 let headers_obj = with_host(|h| {
1359 let mut m = IndexMap::new();
1360 for (k, v) in &headers {
1361 m.insert(k.clone(), h.new_str(v.clone()));
1362 }
1363 h.new_object(m)
1364 });
1365 let raw_headers = flat_headers(&raw_header_pairs);
1370 let mut extra = IndexMap::new();
1371 extra.insert("rawHeaders".into(), raw_headers);
1372 extra.insert("statusCode".into(), Value::Float(status as f64));
1373 extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
1374 extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
1375 extra.insert("headers".into(), headers_obj);
1376 let res = super::net::new_emitter_object("IncomingMessage", extra);
1377
1378 super::events::instance_call(
1379 &entry.request,
1380 "emit",
1381 vec![with_host(|h| h.new_str("response")), res.clone()],
1382 )?;
1383 if !body.is_empty() {
1384 let chunk = super::buffer::from_bytes(&body);
1385 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
1386 }
1387 super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
1388 Ok(())
1389}
1390
1391fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
1393 let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1394 with_host(|h| h.decr_handle());
1395 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1396 if let Some(entry) = entry {
1397 let err = with_host(|h| {
1398 let mut m = IndexMap::new();
1399 m.insert("message".into(), h.new_str(msg.clone()));
1400 h.new_object(m)
1401 });
1402 super::events::instance_call(
1403 &entry.request,
1404 "emit",
1405 vec![with_host(|h| h.new_str("error")), err],
1406 )?;
1407 }
1408 Ok(())
1409}
1410
1411pub(crate) struct ParsedRes {
1415 pub status: u16,
1416 pub message: String,
1417 pub http_version: String,
1418 pub headers: Vec<(String, String)>,
1420 pub raw_headers: Vec<(String, String)>,
1422 pub body: Vec<u8>,
1423}
1424
1425pub(crate) fn parse_raw_response(raw: &[u8]) -> ParsedRes {
1426 let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
1427 let head = String::from_utf8_lossy(&raw[..head_end]);
1428 let body_start = (head_end + 4).min(raw.len());
1429 let mut lines = head.split("\r\n");
1430 let status_line = lines.next().unwrap_or("");
1431 let mut sp = status_line.splitn(3, ' ');
1432 let version = sp
1433 .next()
1434 .unwrap_or("HTTP/1.1")
1435 .strip_prefix("HTTP/")
1436 .unwrap_or("1.1")
1437 .to_string();
1438 let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
1439 let message = sp.next().unwrap_or("").to_string();
1440
1441 let mut headers: Vec<(String, String)> = Vec::new();
1442 let mut raw_headers: Vec<(String, String)> = Vec::new();
1443 let mut chunked = false;
1444 for line in lines {
1445 if line.is_empty() {
1446 continue;
1447 }
1448 if let Some((k, v)) = line.split_once(':') {
1449 let name = k.trim().to_ascii_lowercase();
1450 let value = v.trim().to_string();
1451 if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1452 chunked = true;
1453 }
1454 raw_headers.push((k.trim().to_string(), value.clone()));
1455 headers.push((name, value));
1456 }
1457 }
1458 let raw_body = &raw[body_start..];
1459 let body = if chunked {
1460 decode_chunked(raw_body)
1461 } else {
1462 raw_body.to_vec()
1463 };
1464 ParsedRes {
1465 status,
1466 message,
1467 http_version: version,
1468 headers,
1469 raw_headers,
1470 body,
1471 }
1472}
1473
1474fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1477 let mut out = Vec::new();
1478 while let Some(nl) = find_subslice(data, b"\r\n") {
1479 let size_line = String::from_utf8_lossy(&data[..nl]);
1480 let size_hex = size_line.split(';').next().unwrap_or("").trim();
1481 let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1482 if size == 0 {
1483 break;
1484 }
1485 let chunk_start = nl + 2;
1486 let chunk_end = (chunk_start + size).min(data.len());
1487 out.extend_from_slice(&data[chunk_start..chunk_end]);
1488 let next = chunk_end + 2;
1489 if next >= data.len() {
1490 break;
1491 }
1492 data = &data[next..];
1493 }
1494 out
1495}
1496
1497fn status_text(code: u16) -> &'static str {
1499 match code {
1500 200 => "OK",
1501 201 => "Created",
1502 202 => "Accepted",
1503 204 => "No Content",
1504 301 => "Moved Permanently",
1505 302 => "Found",
1506 304 => "Not Modified",
1507 400 => "Bad Request",
1508 401 => "Unauthorized",
1509 403 => "Forbidden",
1510 404 => "Not Found",
1511 405 => "Method Not Allowed",
1512 409 => "Conflict",
1513 500 => "Internal Server Error",
1514 502 => "Bad Gateway",
1515 503 => "Service Unavailable",
1516 _ => "OK",
1517 }
1518}
1519
1520#[cfg(test)]
1521mod framing_tests {
1522 use super::{response_is_complete, serialize_response, ResState};
1523
1524 #[test]
1527 fn a_keep_alive_content_length_response_is_complete_at_its_last_body_byte() {
1528 let head = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\
1529 Content-Length: 12\r\nConnection: keep-alive\r\n\r\n";
1530 let mut raw = head.to_vec();
1531 assert!(!response_is_complete(&raw, false), "no body yet");
1532 raw.extend_from_slice(b"hello GET /");
1533 assert!(!response_is_complete(&raw, false), "11 of 12 bytes");
1534 raw.push(b'p');
1535 assert!(response_is_complete(&raw, false), "12 of 12 bytes");
1536 }
1537
1538 #[test]
1544 fn a_chunked_response_this_server_wrote_is_complete_to_this_client() {
1545 let mut st = ResState {
1546 sock_id: 0,
1547 head: false,
1548 status: 200,
1549 message: None,
1550 headers: vec![("Transfer-Encoding".into(), "chunked".into())],
1551 body: b"onetwo".to_vec(),
1552 };
1553 let wire = serialize_response(&mut st);
1554 assert!(
1555 wire.ends_with(b"\r\n\r\n6\r\nonetwo\r\n0\r\n\r\n"),
1556 "unexpected framing: {}",
1557 String::from_utf8_lossy(&wire)
1558 );
1559 assert!(response_is_complete(&wire, false));
1560 for cut in 1..wire.len() {
1563 assert!(
1564 !response_is_complete(&wire[..cut], false),
1565 "a {cut}-byte prefix was reported complete"
1566 );
1567 }
1568 }
1569
1570 #[test]
1572 fn an_empty_chunked_response_is_just_the_zero_chunk() {
1573 let mut st = ResState {
1574 sock_id: 0,
1575 head: false,
1576 status: 200,
1577 message: None,
1578 headers: vec![("Transfer-Encoding".into(), "chunked".into())],
1579 body: Vec::new(),
1580 };
1581 let wire = serialize_response(&mut st);
1582 assert!(
1583 wire.ends_with(b"\r\n\r\n0\r\n\r\n"),
1584 "{}",
1585 String::from_utf8_lossy(&wire)
1586 );
1587 assert!(response_is_complete(&wire, false));
1588 }
1589
1590 #[test]
1592 fn a_head_response_advertises_the_get_length_and_sends_no_body() {
1593 let mk = |head: bool| {
1594 let mut st = ResState {
1595 sock_id: 0,
1596 head,
1597 status: 200,
1598 message: None,
1599 headers: vec![("Content-Type".into(), "text/plain".into())],
1600 body: b"body-here".to_vec(),
1601 };
1602 serialize_response(&mut st)
1603 };
1604 let head = mk(true);
1605 let get = mk(false);
1606 let text = String::from_utf8_lossy(&head).into_owned();
1607 assert!(text.contains("Content-Length: 9"), "{text}");
1608 assert!(
1609 text.ends_with("\r\n\r\n"),
1610 "HEAD must stop at the blank line: {text}"
1611 );
1612 assert!(String::from_utf8_lossy(&get).ends_with("body-here"));
1613 let split = |v: &[u8]| {
1615 String::from_utf8_lossy(v)
1616 .split("\r\n\r\n")
1617 .next()
1618 .unwrap()
1619 .to_string()
1620 };
1621 assert_eq!(split(&head), split(&get));
1622 }
1623
1624 #[test]
1625 fn a_partial_header_block_is_never_complete() {
1626 assert!(!response_is_complete(b"", false));
1627 assert!(!response_is_complete(b"HTTP/1.1 200 OK\r\n", false));
1628 assert!(!response_is_complete(
1629 b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n",
1630 false
1631 ));
1632 }
1633
1634 #[test]
1637 fn an_unframed_response_is_never_reported_complete() {
1638 let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nbody bytes";
1639 assert!(!response_is_complete(raw, false));
1640 }
1641
1642 #[test]
1643 fn a_chunked_body_completes_only_at_its_zero_chunk() {
1644 let head = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n";
1645 let mut raw = head.to_vec();
1646 assert!(!response_is_complete(&raw, false));
1647 raw.extend_from_slice(b"5\r\nhello\r\n");
1648 assert!(
1649 !response_is_complete(&raw, false),
1650 "one chunk, no terminator"
1651 );
1652 raw.extend_from_slice(b"0\r\n");
1653 assert!(
1654 !response_is_complete(&raw, false),
1655 "trailer CRLF still missing"
1656 );
1657 raw.extend_from_slice(b"\r\n");
1658 assert!(response_is_complete(&raw, false));
1659 }
1660
1661 #[test]
1664 fn chunk_extensions_parse_and_chunked_outranks_content_length() {
1665 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 99\r\nTransfer-Encoding: chunked\r\n\r\n\
1666 3;name=v\r\nabc\r\n0\r\n\r\n";
1667 assert!(response_is_complete(raw, false));
1668 let short =
1669 b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nTransfer-Encoding: chunked\r\n\r\nabc";
1670 assert!(
1671 !response_is_complete(short, false),
1672 "Content-Length must not settle a chunked response"
1673 );
1674 }
1675
1676 #[test]
1679 fn bodiless_statuses_and_head_finish_at_the_header_block() {
1680 assert!(response_is_complete(
1681 b"HTTP/1.1 204 No Content\r\nContent-Length: 7\r\n\r\n",
1682 false
1683 ));
1684 assert!(response_is_complete(
1685 b"HTTP/1.1 304 Not Modified\r\nContent-Length: 7\r\n\r\n",
1686 false
1687 ));
1688 assert!(response_is_complete(
1689 b"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\n",
1690 true
1691 ));
1692 assert!(
1693 !response_is_complete(b"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\n", false),
1694 "the same response to a GET still needs its body"
1695 );
1696 }
1697
1698 #[test]
1700 fn an_interim_response_does_not_end_the_read() {
1701 assert!(!response_is_complete(
1702 b"HTTP/1.1 100 Continue\r\n\r\n",
1703 false
1704 ));
1705 assert!(!response_is_complete(
1706 b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n",
1707 false
1708 ));
1709 }
1710}