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