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