1use std::cell::RefCell;
2use std::collections::{BTreeMap, HashMap};
3use std::rc::Rc;
4
5use crate::value::{VmClosure, VmError, VmValue};
6use crate::vm::Vm;
7
8mod client;
9pub(crate) mod framing;
10mod mock;
11mod streaming;
12#[cfg(test)]
13mod tests;
14
15use mock::{
16 clear_http_mocks, http_mock_calls_value, parse_mock_responses, register_http_mock,
17 reset_http_mocks,
18};
19pub use mock::{http_mock_calls_snapshot, push_http_mock, HttpMockCallSnapshot, HttpMockResponse};
20
21pub(crate) async fn execute_http_request(
27 method: &str,
28 url: &str,
29 options: &BTreeMap<String, VmValue>,
30) -> Result<VmValue, VmError> {
31 client::vm_execute_http_request(method, url, options).await
32}
33#[cfg(test)]
34use mock::{mock_call_headers_value, redact_mock_call_url};
35
36#[derive(Clone)]
37struct HttpServerRoute {
38 method: String,
39 template: String,
40 handler: Rc<VmClosure>,
41 max_body_bytes: Option<usize>,
42 retain_raw_body: Option<bool>,
43}
44
45#[derive(Clone)]
46struct HttpServer {
47 routes: Vec<HttpServerRoute>,
48 before: Vec<Rc<VmClosure>>,
49 after: Vec<Rc<VmClosure>>,
50 ready: bool,
51 readiness: Option<Rc<VmClosure>>,
52 shutdown_hooks: Vec<Rc<VmClosure>>,
53 shutdown: bool,
54 max_body_bytes: usize,
55 retain_raw_body: bool,
56}
57
58pub(super) const DEFAULT_TIMEOUT_MS: u64 = 30_000;
59pub(super) const DEFAULT_BACKOFF_MS: u64 = 1_000;
60pub(super) const MAX_RETRY_DELAY_MS: u64 = 60_000;
61pub(super) const DEFAULT_RETRYABLE_STATUSES: [u16; 6] = [408, 429, 500, 502, 503, 504];
62pub(super) const DEFAULT_RETRYABLE_METHODS: [&str; 5] = ["GET", "HEAD", "PUT", "DELETE", "OPTIONS"];
63pub(super) const DEFAULT_TRANSPORT_RECEIVE_TIMEOUT_MS: u64 = 30_000;
64pub(super) const DEFAULT_MAX_STREAM_EVENTS: usize = 10_000;
65pub(super) const DEFAULT_MAX_MESSAGE_BYTES: usize = 1024 * 1024;
66pub(super) const DEFAULT_SERVER_MAX_BODY_BYTES: usize = 1024 * 1024;
67pub(super) const DEFAULT_WEBSOCKET_SERVER_IDLE_TIMEOUT_MS: u64 = 30_000;
68pub(super) const MAX_HTTP_SESSIONS: usize = 64;
69pub(super) const MAX_HTTP_STREAMS: usize = 64;
70pub(super) const MAX_SSE_STREAMS: usize = 64;
71pub(super) const MAX_SSE_SERVER_STREAMS: usize = 64;
72pub(super) const MAX_WEBSOCKETS: usize = 64;
73pub(super) const MULTIPART_MOCK_BOUNDARY: &str = "harn-boundary";
74pub(super) const MAX_HTTP_SERVERS: usize = 128;
75pub(super) const MAX_WEBSOCKET_SERVERS: usize = 16;
76
77thread_local! {
78 static TRANSPORT_HANDLE_COUNTER: RefCell<u64> = const { RefCell::new(0) };
79 static HTTP_SERVERS: RefCell<HashMap<String, HttpServer>> = RefCell::new(HashMap::new());
80}
81
82pub fn reset_http_state() {
84 reset_http_mocks();
85 client::reset_client_state();
86 streaming::reset_streaming_state();
87 TRANSPORT_HANDLE_COUNTER.with(|counter| *counter.borrow_mut() = 0);
88 HTTP_SERVERS.with(|servers| servers.borrow_mut().clear());
89}
90
91pub(super) fn vm_error(message: impl Into<String>) -> VmError {
92 VmError::Thrown(VmValue::String(Rc::from(message.into())))
93}
94
95pub(super) fn next_transport_handle(prefix: &str) -> String {
96 TRANSPORT_HANDLE_COUNTER.with(|counter| {
97 let mut counter = counter.borrow_mut();
98 *counter += 1;
99 format!("{prefix}-{}", *counter)
100 })
101}
102
103pub(super) fn handle_from_value(value: &VmValue, builtin: &str) -> Result<String, VmError> {
104 match value {
105 VmValue::String(handle) => Ok(handle.to_string()),
106 VmValue::Dict(dict) => dict
107 .get("id")
108 .map(|id| id.display())
109 .filter(|id| !id.is_empty())
110 .ok_or_else(|| vm_error(format!("{builtin}: handle dict must contain id"))),
111 _ => Err(vm_error(format!(
112 "{builtin}: first argument must be a handle string or dict"
113 ))),
114 }
115}
116
117pub(super) fn get_options_arg(args: &[VmValue], index: usize) -> BTreeMap<String, VmValue> {
118 args.get(index)
119 .and_then(|value| value.as_dict())
120 .cloned()
121 .unwrap_or_default()
122}
123
124fn vm_string(value: impl AsRef<str>) -> VmValue {
125 VmValue::String(Rc::from(value.as_ref()))
126}
127
128fn dict_value(entries: BTreeMap<String, VmValue>) -> VmValue {
129 VmValue::Dict(Rc::new(entries))
130}
131
132fn get_bool_option(options: &BTreeMap<String, VmValue>, key: &str, default: bool) -> bool {
133 match options.get(key) {
134 Some(VmValue::Bool(value)) => *value,
135 _ => default,
136 }
137}
138
139fn get_usize_option(
140 options: &BTreeMap<String, VmValue>,
141 key: &str,
142 default: usize,
143) -> Result<usize, VmError> {
144 match options.get(key).and_then(VmValue::as_int) {
145 Some(value) if value >= 0 => Ok(value as usize),
146 Some(_) => Err(vm_error(format!("http_server: {key} must be non-negative"))),
147 None => Ok(default),
148 }
149}
150
151fn get_optional_usize_option(
152 options: &BTreeMap<String, VmValue>,
153 key: &str,
154) -> Result<Option<usize>, VmError> {
155 match options.get(key).and_then(VmValue::as_int) {
156 Some(value) if value >= 0 => Ok(Some(value as usize)),
157 Some(_) => Err(vm_error(format!(
158 "http_server_route: {key} must be non-negative"
159 ))),
160 None => Ok(None),
161 }
162}
163
164fn server_from_value(value: &VmValue, builtin: &str) -> Result<String, VmError> {
165 handle_from_value(value, builtin)
166}
167
168fn closure_arg(args: &[VmValue], index: usize, builtin: &str) -> Result<Rc<VmClosure>, VmError> {
169 match args.get(index) {
170 Some(VmValue::Closure(closure)) => Ok(closure.clone()),
171 Some(other) => Err(vm_error(format!(
172 "{builtin}: argument {} must be a closure, got {}",
173 index + 1,
174 other.type_name()
175 ))),
176 None => Err(vm_error(format!(
177 "{builtin}: missing closure argument {}",
178 index + 1
179 ))),
180 }
181}
182
183fn http_server_handle_value(id: &str) -> VmValue {
184 let mut dict = BTreeMap::new();
185 dict.insert("id".to_string(), vm_string(id));
186 dict.insert("kind".to_string(), vm_string("http_server"));
187 dict_value(dict)
188}
189
190fn header_lookup_value(headers: &BTreeMap<String, VmValue>, name: &str) -> VmValue {
191 headers
192 .iter()
193 .find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
194 .map(|(_, value)| value.clone())
195 .unwrap_or(VmValue::Nil)
196}
197
198fn headers_from_value(value: &VmValue) -> BTreeMap<String, VmValue> {
199 match value {
200 VmValue::Dict(dict) => dict
201 .get("headers")
202 .and_then(VmValue::as_dict)
203 .map(|headers| {
204 headers
205 .iter()
206 .map(|(key, value)| (key.to_ascii_lowercase(), vm_string(value.display())))
207 .collect()
208 })
209 .unwrap_or_else(|| {
210 dict.iter()
211 .map(|(key, value)| (key.to_ascii_lowercase(), vm_string(value.display())))
212 .collect()
213 }),
214 _ => BTreeMap::new(),
215 }
216}
217
218fn normalize_headers(value: Option<&VmValue>) -> BTreeMap<String, VmValue> {
219 match value.and_then(VmValue::as_dict) {
220 Some(headers) => headers
221 .iter()
222 .map(|(key, value)| (key.to_ascii_lowercase(), vm_string(value.display())))
223 .collect(),
224 None => BTreeMap::new(),
225 }
226}
227
228fn percent_decode(input: &str) -> String {
229 let bytes = input.as_bytes();
230 let mut out = Vec::with_capacity(bytes.len());
231 let mut i = 0;
232 while i < bytes.len() {
233 if bytes[i] == b'+' {
234 out.push(b' ');
235 i += 1;
236 continue;
237 }
238 if bytes[i] == b'%' && i + 2 < bytes.len() {
239 if let (Some(hi), Some(lo)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) {
240 out.push((hi << 4) | lo);
241 i += 3;
242 continue;
243 }
244 }
245 out.push(bytes[i]);
246 i += 1;
247 }
248 String::from_utf8_lossy(&out).into_owned()
249}
250
251fn hex_val(byte: u8) -> Option<u8> {
252 match byte {
253 b'0'..=b'9' => Some(byte - b'0'),
254 b'a'..=b'f' => Some(byte - b'a' + 10),
255 b'A'..=b'F' => Some(byte - b'A' + 10),
256 _ => None,
257 }
258}
259
260fn split_path_and_query(raw_path: &str) -> (String, BTreeMap<String, VmValue>) {
261 let (path, query) = raw_path.split_once('?').unwrap_or((raw_path, ""));
262 let mut query_map = BTreeMap::new();
263 for pair in query.split('&').filter(|part| !part.is_empty()) {
264 let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
265 query_map.insert(percent_decode(key), vm_string(percent_decode(value)));
266 }
267 (
268 if path.is_empty() { "/" } else { path }.to_string(),
269 query_map,
270 )
271}
272
273fn request_body_bytes(input: &BTreeMap<String, VmValue>) -> Vec<u8> {
274 match input.get("raw_body").or_else(|| input.get("body")) {
275 Some(VmValue::Bytes(bytes)) => bytes.as_ref().clone(),
276 Some(value) => value.display().into_bytes(),
277 None => Vec::new(),
278 }
279}
280
281fn request_value(
282 method: &str,
283 path: &str,
284 path_params: BTreeMap<String, VmValue>,
285 mut query: BTreeMap<String, VmValue>,
286 input: &BTreeMap<String, VmValue>,
287 body_bytes: &[u8],
288 retain_raw_body: bool,
289) -> VmValue {
290 if let Some(explicit_query) = input.get("query").and_then(VmValue::as_dict) {
291 query.extend(
292 explicit_query
293 .iter()
294 .map(|(key, value)| (key.clone(), value.clone())),
295 );
296 }
297
298 let headers = normalize_headers(input.get("headers"));
299 let body = String::from_utf8_lossy(body_bytes).into_owned();
300 let mut request = BTreeMap::new();
301 request.insert("method".to_string(), vm_string(method));
302 request.insert("path".to_string(), vm_string(path));
303 let path_params = dict_value(path_params);
304 request.insert("path_params".to_string(), path_params.clone());
305 request.insert("params".to_string(), path_params);
306 request.insert("query".to_string(), dict_value(query));
307 request.insert("headers".to_string(), dict_value(headers));
308 request.insert("body".to_string(), vm_string(body));
309 request.insert(
310 "raw_body".to_string(),
311 if retain_raw_body {
312 VmValue::Bytes(Rc::new(body_bytes.to_vec()))
313 } else {
314 VmValue::Nil
315 },
316 );
317 request.insert(
318 "body_bytes".to_string(),
319 VmValue::Int(body_bytes.len() as i64),
320 );
321 request.insert(
322 "remote_addr".to_string(),
323 input
324 .get("remote_addr")
325 .or_else(|| input.get("remote"))
326 .map(|value| vm_string(value.display()))
327 .unwrap_or(VmValue::Nil),
328 );
329 request.insert(
330 "client_ip".to_string(),
331 input
332 .get("client_ip")
333 .or_else(|| input.get("remote_ip"))
334 .or_else(|| input.get("ip"))
335 .map(|value| vm_string(value.display()))
336 .unwrap_or(VmValue::Nil),
337 );
338 dict_value(request)
339}
340
341fn normalize_status(status: i64) -> i64 {
342 if (100..=999).contains(&status) {
343 status
344 } else {
345 500
346 }
347}
348
349fn response_with_kind(
350 status: i64,
351 mut headers: BTreeMap<String, VmValue>,
352 body: VmValue,
353 body_kind: &str,
354) -> VmValue {
355 let status = normalize_status(status);
356 let mut response = BTreeMap::new();
357 if body_kind == "json" && matches!(header_lookup_value(&headers, "content-type"), VmValue::Nil)
358 {
359 headers.insert(
360 "content-type".to_string(),
361 vm_string("application/json; charset=utf-8"),
362 );
363 } else if body_kind == "text"
364 && matches!(header_lookup_value(&headers, "content-type"), VmValue::Nil)
365 {
366 headers.insert(
367 "content-type".to_string(),
368 vm_string("text/plain; charset=utf-8"),
369 );
370 }
371 response.insert("status".to_string(), VmValue::Int(status));
372 response.insert("headers".to_string(), dict_value(headers));
373 response.insert(
374 "ok".to_string(),
375 VmValue::Bool((200..300).contains(&status)),
376 );
377 response.insert("body_kind".to_string(), vm_string(body_kind));
378 match body {
379 VmValue::Bytes(bytes) => {
380 response.insert(
381 "body".to_string(),
382 vm_string(String::from_utf8_lossy(&bytes)),
383 );
384 response.insert("raw_body".to_string(), VmValue::Bytes(bytes));
385 }
386 other => {
387 response.insert("body".to_string(), vm_string(other.display()));
388 response.insert(
389 "raw_body".to_string(),
390 VmValue::Bytes(Rc::new(other.display().into_bytes())),
391 );
392 }
393 }
394 dict_value(response)
395}
396
397fn normalize_response(value: VmValue) -> VmValue {
398 match value {
399 VmValue::Dict(dict) if dict.contains_key("status") => {
400 let status = dict.get("status").and_then(VmValue::as_int).unwrap_or(200);
401 let headers = dict
402 .get("headers")
403 .and_then(VmValue::as_dict)
404 .cloned()
405 .unwrap_or_default();
406 let body_kind = dict
407 .get("body_kind")
408 .or_else(|| dict.get("kind"))
409 .map(|value| value.display())
410 .unwrap_or_else(|| "text".to_string());
411 let body = dict
412 .get("raw_body")
413 .filter(|value| matches!(value, VmValue::Bytes(_)))
414 .or_else(|| dict.get("body"))
415 .cloned()
416 .unwrap_or(VmValue::Nil);
417 response_with_kind(status, headers, body, &body_kind)
418 }
419 VmValue::Nil => response_with_kind(204, BTreeMap::new(), VmValue::Nil, "text"),
420 other => response_with_kind(200, BTreeMap::new(), other, "text"),
421 }
422}
423
424fn body_limit_response(limit: usize, actual: usize) -> VmValue {
425 let mut headers = BTreeMap::new();
426 headers.insert(
427 "content-type".to_string(),
428 vm_string("text/plain; charset=utf-8"),
429 );
430 headers.insert("connection".to_string(), vm_string("close"));
431 headers.insert(
432 "x-harn-body-limit".to_string(),
433 vm_string(limit.to_string()),
434 );
435 response_with_kind(
436 413,
437 headers,
438 vm_string(format!("request body too large: {actual} > {limit} bytes")),
439 "text",
440 )
441}
442
443fn not_found_response(method: &str, path: &str) -> VmValue {
444 response_with_kind(
445 404,
446 BTreeMap::new(),
447 vm_string(format!("no route for {method} {path}")),
448 "text",
449 )
450}
451
452fn unavailable_response(message: &str) -> VmValue {
453 response_with_kind(503, BTreeMap::new(), vm_string(message), "text")
454}
455
456fn route_template_match(template: &str, path: &str) -> Option<BTreeMap<String, VmValue>> {
457 let template_segments: Vec<&str> = template.trim_matches('/').split('/').collect();
458 let path_segments: Vec<&str> = path.trim_matches('/').split('/').collect();
459 if template == "/" && path == "/" {
460 return Some(BTreeMap::new());
461 }
462 if template_segments.len() != path_segments.len() {
463 return None;
464 }
465 let mut params = BTreeMap::new();
466 for (tmpl, actual) in template_segments.iter().zip(path_segments.iter()) {
467 if tmpl.starts_with('{') && tmpl.ends_with('}') && tmpl.len() > 2 {
468 params.insert(
469 tmpl[1..tmpl.len() - 1].to_string(),
470 vm_string(percent_decode(actual)),
471 );
472 } else if tmpl.starts_with(':') && tmpl.len() > 1 {
473 params.insert(tmpl[1..].to_string(), vm_string(percent_decode(actual)));
474 } else if tmpl != actual {
475 return None;
476 }
477 }
478 Some(params)
479}
480
481fn matching_route(
482 server: &HttpServer,
483 method: &str,
484 path: &str,
485) -> Option<(HttpServerRoute, BTreeMap<String, VmValue>)> {
486 server.routes.iter().find_map(|route| {
487 if route.method != "*" && !route.method.eq_ignore_ascii_case(method) {
488 return None;
489 }
490 route_template_match(&route.template, path).map(|params| (route.clone(), params))
491 })
492}
493
494async fn call_server_closure(
495 closure: &Rc<VmClosure>,
496 args: &[VmValue],
497 builtin: &str,
498) -> Result<VmValue, VmError> {
499 let mut vm = crate::vm::clone_async_builtin_child_vm()
500 .ok_or_else(|| vm_error(format!("{builtin}: requires an async builtin VM context")))?;
501 vm.call_closure_pub(closure, args).await
502}
503
504fn value_is_response(value: &VmValue) -> bool {
505 matches!(value, VmValue::Dict(dict) if dict.contains_key("status"))
506}
507
508async fn run_http_server_request(server_id: &str, request: VmValue) -> Result<VmValue, VmError> {
509 let server = HTTP_SERVERS.with(|servers| servers.borrow().get(server_id).cloned());
510 let Some(server) = server else {
511 return Err(vm_error(format!(
512 "http_server_request: unknown server handle '{server_id}'"
513 )));
514 };
515 if server.shutdown {
516 return Ok(unavailable_response("server is shut down"));
517 }
518 if !server.ready {
519 return Ok(unavailable_response("server is not ready"));
520 }
521 if let Some(readiness) = &server.readiness {
522 let ready = call_server_closure(
523 readiness,
524 &[http_server_handle_value(server_id)],
525 "http_server_request",
526 )
527 .await?;
528 if !ready.is_truthy() {
529 return Ok(unavailable_response("server is not ready"));
530 }
531 }
532
533 let input = request.as_dict().cloned().unwrap_or_default();
534 let method = input
535 .get("method")
536 .map(|value| value.display())
537 .filter(|value| !value.is_empty())
538 .unwrap_or_else(|| "GET".to_string())
539 .to_ascii_uppercase();
540 let raw_path = input
541 .get("path")
542 .map(|value| value.display())
543 .filter(|value| !value.is_empty())
544 .unwrap_or_else(|| "/".to_string());
545 let (path, query) = split_path_and_query(&raw_path);
546 let body_bytes = request_body_bytes(&input);
547
548 let Some((route, path_params)) = matching_route(&server, &method, &path) else {
549 return Ok(not_found_response(&method, &path));
550 };
551
552 let limit = route.max_body_bytes.unwrap_or(server.max_body_bytes);
553 if body_bytes.len() > limit {
554 return Ok(body_limit_response(limit, body_bytes.len()));
555 }
556 let retain_raw_body = route.retain_raw_body.unwrap_or(server.retain_raw_body);
557 let mut req = request_value(
558 &method,
559 &path,
560 path_params,
561 query,
562 &input,
563 &body_bytes,
564 retain_raw_body,
565 );
566
567 for before in &server.before {
568 let result = call_server_closure(before, &[req.clone()], "http_server_request").await?;
569 if value_is_response(&result) {
570 return Ok(normalize_response(result));
571 }
572 if !matches!(result, VmValue::Nil) {
573 req = result;
574 }
575 }
576
577 let handler_result =
578 call_server_closure(&route.handler, &[req.clone()], "http_server_request").await?;
579 let mut response = normalize_response(handler_result);
580
581 for after in &server.after {
582 let result = call_server_closure(
583 after,
584 &[response.clone(), req.clone()],
585 "http_server_request",
586 )
587 .await?;
588 if !matches!(result, VmValue::Nil) {
589 response = normalize_response(result);
590 }
591 }
592
593 Ok(response)
594}
595
596pub fn register_http_builtins(vm: &mut Vm) {
598 register_http_tls_builtins(vm);
599 client::register_http_verb_builtins(vm);
600 register_http_server_builtins(vm);
601 register_http_mock_builtins(vm);
602 client::register_http_client_builtins(vm);
603 streaming::register_http_streaming_builtins(vm);
604}
605
606fn register_http_tls_builtins(vm: &mut Vm) {
607 vm.register_builtin("http_server_tls_plain", |_args, _out| {
608 Ok(http_server_tls_config_value(
609 "plain",
610 false,
611 "http",
612 false,
613 BTreeMap::new(),
614 ))
615 });
616 vm.register_builtin("http_server_tls_edge", |args, _out| {
617 let options = get_options_arg(args, 0);
618 Ok(http_server_tls_config_value(
619 "edge",
620 false,
621 "https",
622 vm_get_bool_option(&options, "hsts", true),
623 hsts_options(&options),
624 ))
625 });
626 vm.register_builtin("http_server_tls_pem", |args, _out| {
627 if args.len() < 2 {
628 return Err(vm_error(
629 "http_server_tls_pem: requires cert path and key path",
630 ));
631 }
632 let cert_path = args[0].display();
633 let key_path = args[1].display();
634 if !std::path::Path::new(&cert_path).is_file() {
635 return Err(vm_error(format!(
636 "http_server_tls_pem: certificate not found: {cert_path}"
637 )));
638 }
639 if !std::path::Path::new(&key_path).is_file() {
640 return Err(vm_error(format!(
641 "http_server_tls_pem: private key not found: {key_path}"
642 )));
643 }
644 let mut extra = BTreeMap::new();
645 extra.insert(
646 "cert_path".to_string(),
647 VmValue::String(Rc::from(cert_path)),
648 );
649 extra.insert("key_path".to_string(), VmValue::String(Rc::from(key_path)));
650 Ok(http_server_tls_config_value(
651 "pem", true, "https", true, extra,
652 ))
653 });
654 vm.register_builtin("http_server_tls_self_signed_dev", |args, _out| {
655 let hosts = tls_hosts_arg(args.first())?;
656 let cert = rcgen::generate_simple_self_signed(hosts.clone()).map_err(|error| {
657 vm_error(format!(
658 "http_server_tls_self_signed_dev: failed to generate certificate: {error}"
659 ))
660 })?;
661 let mut extra = BTreeMap::new();
662 extra.insert(
663 "hosts".to_string(),
664 VmValue::List(Rc::new(
665 hosts
666 .into_iter()
667 .map(|host| VmValue::String(Rc::from(host)))
668 .collect(),
669 )),
670 );
671 extra.insert(
672 "cert_pem".to_string(),
673 VmValue::String(Rc::from(cert.cert.pem())),
674 );
675 extra.insert(
676 "key_pem".to_string(),
677 VmValue::String(Rc::from(cert.signing_key.serialize_pem())),
678 );
679 Ok(http_server_tls_config_value(
680 "self_signed_dev",
681 true,
682 "https",
683 false,
684 extra,
685 ))
686 });
687 vm.register_builtin("http_server_security_headers", |args, _out| {
688 let Some(VmValue::Dict(config)) = args.first() else {
689 return Err(vm_error(
690 "http_server_security_headers: requires a TLS config dict",
691 ));
692 };
693 Ok(VmValue::Dict(Rc::new(http_server_security_headers(config))))
694 });
695}
696
697fn register_http_server_builtins(vm: &mut Vm) {
698 vm.register_builtin("http_server", |args, _out| {
701 let options = get_options_arg(args, 0);
702 let server = HttpServer {
703 routes: Vec::new(),
704 before: Vec::new(),
705 after: Vec::new(),
706 ready: get_bool_option(&options, "ready", true),
707 readiness: None,
708 shutdown_hooks: Vec::new(),
709 shutdown: false,
710 max_body_bytes: get_usize_option(
711 &options,
712 "max_body_bytes",
713 DEFAULT_SERVER_MAX_BODY_BYTES,
714 )?,
715 retain_raw_body: get_bool_option(&options, "retain_raw_body", true),
716 };
717 let id = next_transport_handle("http-server");
718 HTTP_SERVERS.with(|servers| {
719 let mut servers = servers.borrow_mut();
720 if servers.len() >= MAX_HTTP_SERVERS {
721 return Err(vm_error(format!(
722 "http_server: maximum open servers ({MAX_HTTP_SERVERS}) reached"
723 )));
724 }
725 servers.insert(id.clone(), server);
726 Ok(())
727 })?;
728 Ok(http_server_handle_value(&id))
729 });
730
731 vm.register_builtin("http_server_route", |args, _out| {
732 if args.len() < 4 {
733 return Err(vm_error(
734 "http_server_route: requires server, method, path template, and handler",
735 ));
736 }
737 let server_id = server_from_value(&args[0], "http_server_route")?;
738 let method = args[1].display().to_ascii_uppercase();
739 if method.is_empty() {
740 return Err(vm_error("http_server_route: method is required"));
741 }
742 let template = args[2].display();
743 if !template.starts_with('/') {
744 return Err(vm_error(
745 "http_server_route: path template must start with '/'",
746 ));
747 }
748 let handler = closure_arg(args, 3, "http_server_route")?;
749 let options = get_options_arg(args, 4);
750 let route = HttpServerRoute {
751 method,
752 template,
753 handler,
754 max_body_bytes: get_optional_usize_option(&options, "max_body_bytes")?,
755 retain_raw_body: match options.get("retain_raw_body") {
756 Some(VmValue::Bool(value)) => Some(*value),
757 _ => None,
758 },
759 };
760 HTTP_SERVERS.with(|servers| {
761 let mut servers = servers.borrow_mut();
762 let server = servers.get_mut(&server_id).ok_or_else(|| {
763 vm_error(format!("http_server_route: unknown server '{server_id}'"))
764 })?;
765 server.routes.push(route);
766 Ok::<_, VmError>(())
767 })?;
768 Ok(http_server_handle_value(&server_id))
769 });
770
771 vm.register_builtin("http_server_before", |args, _out| {
772 if args.len() < 2 {
773 return Err(vm_error("http_server_before: requires server and handler"));
774 }
775 let server_id = server_from_value(&args[0], "http_server_before")?;
776 let handler = closure_arg(args, 1, "http_server_before")?;
777 HTTP_SERVERS.with(|servers| {
778 let mut servers = servers.borrow_mut();
779 let server = servers.get_mut(&server_id).ok_or_else(|| {
780 vm_error(format!("http_server_before: unknown server '{server_id}'"))
781 })?;
782 server.before.push(handler);
783 Ok::<_, VmError>(())
784 })?;
785 Ok(http_server_handle_value(&server_id))
786 });
787
788 vm.register_builtin("http_server_after", |args, _out| {
789 if args.len() < 2 {
790 return Err(vm_error("http_server_after: requires server and handler"));
791 }
792 let server_id = server_from_value(&args[0], "http_server_after")?;
793 let handler = closure_arg(args, 1, "http_server_after")?;
794 HTTP_SERVERS.with(|servers| {
795 let mut servers = servers.borrow_mut();
796 let server = servers.get_mut(&server_id).ok_or_else(|| {
797 vm_error(format!("http_server_after: unknown server '{server_id}'"))
798 })?;
799 server.after.push(handler);
800 Ok::<_, VmError>(())
801 })?;
802 Ok(http_server_handle_value(&server_id))
803 });
804
805 vm.register_async_builtin("http_server_request", |args| async move {
806 if args.len() < 2 {
807 return Err(vm_error("http_server_request: requires server and request"));
808 }
809 let server_id = server_from_value(&args[0], "http_server_request")?;
810 run_http_server_request(&server_id, args[1].clone()).await
811 });
812
813 vm.register_async_builtin("http_server_test", |args| async move {
814 if args.len() < 2 {
815 return Err(vm_error("http_server_test: requires server and request"));
816 }
817 let server_id = server_from_value(&args[0], "http_server_test")?;
818 run_http_server_request(&server_id, args[1].clone()).await
819 });
820
821 vm.register_builtin("http_server_set_ready", |args, _out| {
822 if args.len() < 2 {
823 return Err(vm_error(
824 "http_server_set_ready: requires server and ready bool",
825 ));
826 }
827 let server_id = server_from_value(&args[0], "http_server_set_ready")?;
828 let ready = matches!(args[1], VmValue::Bool(true));
829 HTTP_SERVERS.with(|servers| {
830 let mut servers = servers.borrow_mut();
831 let server = servers.get_mut(&server_id).ok_or_else(|| {
832 vm_error(format!(
833 "http_server_set_ready: unknown server '{server_id}'"
834 ))
835 })?;
836 server.ready = ready;
837 Ok::<_, VmError>(())
838 })?;
839 Ok(VmValue::Bool(ready))
840 });
841
842 vm.register_builtin("http_server_readiness", |args, _out| {
843 if args.len() < 2 {
844 return Err(vm_error(
845 "http_server_readiness: requires server and readiness closure",
846 ));
847 }
848 let server_id = server_from_value(&args[0], "http_server_readiness")?;
849 let handler = closure_arg(args, 1, "http_server_readiness")?;
850 HTTP_SERVERS.with(|servers| {
851 let mut servers = servers.borrow_mut();
852 let server = servers.get_mut(&server_id).ok_or_else(|| {
853 vm_error(format!(
854 "http_server_readiness: unknown server '{server_id}'"
855 ))
856 })?;
857 server.readiness = Some(handler);
858 Ok::<_, VmError>(())
859 })?;
860 Ok(http_server_handle_value(&server_id))
861 });
862
863 vm.register_async_builtin("http_server_ready", |args| async move {
864 let Some(server_arg) = args.first() else {
865 return Err(vm_error("http_server_ready: requires server"));
866 };
867 let server_id = server_from_value(server_arg, "http_server_ready")?;
868 let server = HTTP_SERVERS.with(|servers| servers.borrow().get(&server_id).cloned());
869 let Some(server) = server else {
870 return Err(vm_error(format!(
871 "http_server_ready: unknown server '{server_id}'"
872 )));
873 };
874 if server.shutdown {
875 return Ok(VmValue::Bool(false));
876 }
877 let Some(readiness) = server.readiness else {
878 return Ok(VmValue::Bool(server.ready));
879 };
880 let result = call_server_closure(
881 &readiness,
882 &[http_server_handle_value(&server_id)],
883 "http_server_ready",
884 )
885 .await?;
886 Ok(VmValue::Bool(result.is_truthy()))
887 });
888
889 vm.register_builtin("http_server_on_shutdown", |args, _out| {
890 if args.len() < 2 {
891 return Err(vm_error(
892 "http_server_on_shutdown: requires server and handler",
893 ));
894 }
895 let server_id = server_from_value(&args[0], "http_server_on_shutdown")?;
896 let handler = closure_arg(args, 1, "http_server_on_shutdown")?;
897 HTTP_SERVERS.with(|servers| {
898 let mut servers = servers.borrow_mut();
899 let server = servers.get_mut(&server_id).ok_or_else(|| {
900 vm_error(format!(
901 "http_server_on_shutdown: unknown server '{server_id}'"
902 ))
903 })?;
904 server.shutdown_hooks.push(handler);
905 Ok::<_, VmError>(())
906 })?;
907 Ok(http_server_handle_value(&server_id))
908 });
909
910 vm.register_async_builtin("http_server_shutdown", |args| async move {
911 let Some(server_arg) = args.first() else {
912 return Err(vm_error("http_server_shutdown: requires server"));
913 };
914 let server_id = server_from_value(server_arg, "http_server_shutdown")?;
915 let hooks = HTTP_SERVERS.with(|servers| {
916 let mut servers = servers.borrow_mut();
917 let server = servers.get_mut(&server_id).ok_or_else(|| {
918 vm_error(format!(
919 "http_server_shutdown: unknown server '{server_id}'"
920 ))
921 })?;
922 server.shutdown = true;
923 Ok::<_, VmError>(server.shutdown_hooks.clone())
924 })?;
925 for hook in hooks {
926 let _ = call_server_closure(
927 &hook,
928 &[http_server_handle_value(&server_id)],
929 "http_server_shutdown",
930 )
931 .await?;
932 }
933 Ok(VmValue::Bool(true))
934 });
935
936 vm.register_builtin("http_response", |args, _out| {
937 let status = args.first().and_then(VmValue::as_int).unwrap_or(200);
938 let body = args.get(1).cloned().unwrap_or(VmValue::Nil);
939 let headers = args
940 .get(2)
941 .and_then(VmValue::as_dict)
942 .cloned()
943 .unwrap_or_default();
944 Ok(response_with_kind(status, headers, body, "text"))
945 });
946
947 vm.register_builtin("http_response_text", |args, _out| {
948 let body = args.first().cloned().unwrap_or(VmValue::Nil);
949 let options = get_options_arg(args, 1);
950 let status = options
951 .get("status")
952 .and_then(VmValue::as_int)
953 .unwrap_or(200);
954 let headers = options
955 .get("headers")
956 .and_then(VmValue::as_dict)
957 .cloned()
958 .unwrap_or_default();
959 Ok(response_with_kind(status, headers, body, "text"))
960 });
961
962 vm.register_builtin("http_response_json", |args, _out| {
963 let body = args
964 .first()
965 .map(crate::stdlib::json::vm_value_to_json)
966 .map(vm_string)
967 .unwrap_or_else(|| vm_string("null"));
968 let options = get_options_arg(args, 1);
969 let status = options
970 .get("status")
971 .and_then(VmValue::as_int)
972 .unwrap_or(200);
973 let headers = options
974 .get("headers")
975 .and_then(VmValue::as_dict)
976 .cloned()
977 .unwrap_or_default();
978 Ok(response_with_kind(status, headers, body, "json"))
979 });
980
981 vm.register_builtin("http_response_bytes", |args, _out| {
982 let body = match args.first() {
983 Some(VmValue::Bytes(bytes)) => VmValue::Bytes(bytes.clone()),
984 Some(value) => VmValue::Bytes(Rc::new(value.display().into_bytes())),
985 None => VmValue::Bytes(Rc::new(Vec::new())),
986 };
987 let options = get_options_arg(args, 1);
988 let status = options
989 .get("status")
990 .and_then(VmValue::as_int)
991 .unwrap_or(200);
992 let headers = options
993 .get("headers")
994 .and_then(VmValue::as_dict)
995 .cloned()
996 .unwrap_or_default();
997 Ok(response_with_kind(status, headers, body, "bytes"))
998 });
999
1000 vm.register_builtin("http_header", |args, _out| {
1001 if args.len() < 2 {
1002 return Err(vm_error(
1003 "http_header: requires headers/request/response and name",
1004 ));
1005 }
1006 let headers = headers_from_value(&args[0]);
1007 Ok(header_lookup_value(&headers, &args[1].display()))
1008 });
1009}
1010
1011fn register_http_mock_builtins(vm: &mut Vm) {
1012 vm.register_builtin("http_mock", |args, _out| {
1020 let method = args.first().map(|a| a.display()).unwrap_or_default();
1021 let url_pattern = args.get(1).map(|a| a.display()).unwrap_or_default();
1022 let response = args
1023 .get(2)
1024 .and_then(|a| a.as_dict())
1025 .cloned()
1026 .unwrap_or_default();
1027 let responses = parse_mock_responses(&response);
1028
1029 register_http_mock(method, url_pattern, responses);
1030 Ok(VmValue::Nil)
1031 });
1032
1033 vm.register_builtin("http_mock_clear", |_args, _out| {
1035 clear_http_mocks();
1036 client::clear_http_streams();
1037 Ok(VmValue::Nil)
1038 });
1039
1040 vm.register_builtin("http_mock_calls", |args, _out| {
1042 let options = get_options_arg(args, 0);
1043 let include_sensitive = get_bool_option(&options, "include_sensitive", false)
1044 || get_bool_option(&options, "include_sensitive_headers", false);
1045 let redact_sensitive = get_bool_option(
1046 &options,
1047 "redact_sensitive",
1048 get_bool_option(&options, "redact_headers", true),
1049 ) && !include_sensitive;
1050 Ok(VmValue::List(Rc::new(http_mock_calls_value(
1051 redact_sensitive,
1052 ))))
1053 });
1054}
1055
1056fn http_server_tls_config_value(
1057 mode: &str,
1058 terminate_tls: bool,
1059 scheme: &str,
1060 hsts: bool,
1061 extra: BTreeMap<String, VmValue>,
1062) -> VmValue {
1063 let mut dict = BTreeMap::new();
1064 dict.insert("mode".to_string(), VmValue::String(Rc::from(mode)));
1065 dict.insert("terminate_tls".to_string(), VmValue::Bool(terminate_tls));
1066 dict.insert("scheme".to_string(), VmValue::String(Rc::from(scheme)));
1067 dict.insert("hsts".to_string(), VmValue::Bool(hsts));
1068 for (key, value) in extra {
1069 dict.insert(key, value);
1070 }
1071 VmValue::Dict(Rc::new(dict))
1072}
1073
1074fn hsts_options(options: &BTreeMap<String, VmValue>) -> BTreeMap<String, VmValue> {
1075 let mut hsts = BTreeMap::new();
1076 hsts.insert(
1077 "hsts_max_age_seconds".to_string(),
1078 VmValue::Int(vm_get_int_option(
1079 options,
1080 "hsts_max_age_seconds",
1081 31_536_000,
1082 )),
1083 );
1084 hsts.insert(
1085 "hsts_include_subdomains".to_string(),
1086 VmValue::Bool(vm_get_bool_option(
1087 options,
1088 "hsts_include_subdomains",
1089 false,
1090 )),
1091 );
1092 hsts.insert(
1093 "hsts_preload".to_string(),
1094 VmValue::Bool(vm_get_bool_option(options, "hsts_preload", false)),
1095 );
1096 hsts
1097}
1098
1099fn http_server_security_headers(config: &BTreeMap<String, VmValue>) -> BTreeMap<String, VmValue> {
1100 let hsts_enabled = vm_get_bool_option(config, "hsts", false);
1101 if !hsts_enabled {
1102 return BTreeMap::new();
1103 }
1104 let mut value = format!(
1105 "max-age={}",
1106 vm_get_int_option(config, "hsts_max_age_seconds", 31_536_000).max(0)
1107 );
1108 if vm_get_bool_option(config, "hsts_include_subdomains", false) {
1109 value.push_str("; includeSubDomains");
1110 }
1111 if vm_get_bool_option(config, "hsts_preload", false) {
1112 value.push_str("; preload");
1113 }
1114 BTreeMap::from([(
1115 "strict-transport-security".to_string(),
1116 VmValue::String(Rc::from(value)),
1117 )])
1118}
1119
1120fn tls_hosts_arg(value: Option<&VmValue>) -> Result<Vec<String>, VmError> {
1121 match value {
1122 None | Some(VmValue::Nil) => Ok(vec!["localhost".to_string(), "127.0.0.1".to_string()]),
1123 Some(VmValue::List(hosts)) => {
1124 let mut parsed = Vec::new();
1125 for host in hosts.iter() {
1126 let host = host.display();
1127 if host.is_empty() {
1128 return Err(vm_error(
1129 "http_server_tls_self_signed_dev: host names must be non-empty",
1130 ));
1131 }
1132 parsed.push(host);
1133 }
1134 if parsed.is_empty() {
1135 return Err(vm_error(
1136 "http_server_tls_self_signed_dev: host list must not be empty",
1137 ));
1138 }
1139 Ok(parsed)
1140 }
1141 Some(other) => {
1142 let host = other.display();
1143 if host.is_empty() {
1144 return Err(vm_error(
1145 "http_server_tls_self_signed_dev: host name must be non-empty",
1146 ));
1147 }
1148 Ok(vec![host])
1149 }
1150 }
1151}
1152
1153pub(super) fn vm_get_int_option(
1154 options: &BTreeMap<String, VmValue>,
1155 key: &str,
1156 default: i64,
1157) -> i64 {
1158 options.get(key).and_then(|v| v.as_int()).unwrap_or(default)
1159}
1160
1161pub(super) fn vm_get_bool_option(
1162 options: &BTreeMap<String, VmValue>,
1163 key: &str,
1164 default: bool,
1165) -> bool {
1166 match options.get(key) {
1167 Some(VmValue::Bool(b)) => *b,
1168 _ => default,
1169 }
1170}
1171
1172pub(super) fn vm_get_int_option_prefer(
1173 options: &BTreeMap<String, VmValue>,
1174 canonical: &str,
1175 alias: &str,
1176 default: i64,
1177) -> i64 {
1178 options
1179 .get(canonical)
1180 .and_then(|value| value.as_int())
1181 .or_else(|| options.get(alias).and_then(|value| value.as_int()))
1182 .unwrap_or(default)
1183}
1184
1185pub(super) fn vm_get_optional_int_option(
1186 options: &BTreeMap<String, VmValue>,
1187 key: &str,
1188) -> Option<u64> {
1189 options
1190 .get(key)
1191 .and_then(|value| value.as_int())
1192 .map(|value| value.max(0) as u64)
1193}
1194
1195pub(super) fn string_option(options: &BTreeMap<String, VmValue>, key: &str) -> Option<String> {
1196 options
1197 .get(key)
1198 .map(|value| value.display())
1199 .filter(|value| !value.is_empty())
1200}