1use axum::{
14 body::Body,
15 debug_handler,
16 extract::{Path, State},
17 http::{Request, StatusCode},
18 response::{IntoResponse, Response},
19 Json,
20};
21use microsandbox_core::management::{menv, orchestra};
22use microsandbox_utils::{DEFAULT_CONFIG, DEFAULT_PORTAL_GUEST_PORT, MICROSANDBOX_CONFIG_FILENAME};
23use reqwest;
24use serde_json::{self, json};
25use serde_yaml;
26use std::path::PathBuf;
27use tokio::fs as tokio_fs;
28use tokio::time::{sleep, timeout, Duration};
29use tracing::{debug, trace, warn};
30
31use crate::{
32 error::ServerError,
33 middleware,
34 payload::{
35 JsonRpcError, JsonRpcRequest, JsonRpcResponse, RegularMessageResponse,
36 SandboxMetricsGetParams, SandboxStartParams, SandboxStopParams, JSONRPC_VERSION,
37 },
38 state::AppState,
39 SandboxStatus, SandboxStatusResponse, ServerResult,
40};
41
42pub async fn health() -> ServerResult<impl IntoResponse> {
48 Ok((
49 StatusCode::OK,
50 Json(RegularMessageResponse {
51 message: "Service is healthy".to_string(),
52 }),
53 ))
54}
55
56#[debug_handler]
62pub async fn json_rpc_handler(
63 State(state): State<AppState>,
64 Json(request): Json<JsonRpcRequest>,
65) -> ServerResult<impl IntoResponse> {
66 debug!(?request, "Received JSON-RPC request");
67
68 if request.jsonrpc != JSONRPC_VERSION {
70 let error = JsonRpcError {
71 code: -32600,
72 message: "Invalid or missing jsonrpc version field".to_string(),
73 data: None,
74 };
75 return Ok((
76 StatusCode::BAD_REQUEST,
77 Json(JsonRpcResponse::error(error, request.id.clone())),
78 ));
79 }
80
81 let method = request.method.as_str();
82 let id = request.id.clone();
83
84 match method {
85 "sandbox.start" => {
87 let start_params: SandboxStartParams =
89 serde_json::from_value(request.params.clone()).map_err(|e| {
90 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
91 format!("Invalid params for sandbox.start: {}", e),
92 ))
93 })?;
94
95 let result = sandbox_start_impl(state, start_params).await?;
97
98 Ok((
100 StatusCode::OK,
101 Json(JsonRpcResponse::success(json!(result), id)),
102 ))
103 }
104 "sandbox.stop" => {
105 let stop_params: SandboxStopParams = serde_json::from_value(request.params.clone())
107 .map_err(|e| {
108 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
109 format!("Invalid params for sandbox.stop: {}", e),
110 ))
111 })?;
112
113 let result = sandbox_stop_impl(state, stop_params).await?;
115
116 Ok((
118 StatusCode::OK,
119 Json(JsonRpcResponse::success(json!(result), id)),
120 ))
121 }
122 "sandbox.metrics.get" => {
123 let metrics_params: SandboxMetricsGetParams =
125 serde_json::from_value(request.params.clone()).map_err(|e| {
126 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
127 format!("Invalid params for sandbox.metrics.get: {}", e),
128 ))
129 })?;
130
131 let result = sandbox_get_metrics_impl(state.clone(), metrics_params).await?;
133
134 Ok((
136 StatusCode::OK,
137 Json(JsonRpcResponse::success(json!(result), id)),
138 ))
139 }
140
141 "sandbox.repl.run" | "sandbox.command.run" => {
143 forward_rpc_to_portal(state, request).await
145 }
146
147 _ => {
148 let error = JsonRpcError {
149 code: -32601,
150 message: format!("Method not found: {}", method),
151 data: None,
152 };
153 Ok((
154 StatusCode::NOT_FOUND,
155 Json(JsonRpcResponse::error(error, id)),
156 ))
157 }
158 }
159}
160
161async fn forward_rpc_to_portal(
163 state: AppState,
164 request: JsonRpcRequest,
165) -> ServerResult<(StatusCode, Json<JsonRpcResponse>)> {
166 let (sandbox_name, namespace) = if let Some(params) = request.params.as_object() {
172 let sandbox = params
174 .get("sandbox")
175 .and_then(|v| v.as_str())
176 .ok_or_else(|| {
177 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
178 "Missing required 'sandbox' parameter for portal request".to_string(),
179 ))
180 })?;
181
182 let namespace = params
184 .get("namespace")
185 .and_then(|v| v.as_str())
186 .ok_or_else(|| {
187 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
188 "Missing required 'namespace' parameter for portal request".to_string(),
189 ))
190 })?;
191
192 (sandbox, namespace)
193 } else {
194 return Err(ServerError::ValidationError(
195 crate::error::ValidationError::InvalidInput(
196 "Request parameters must be an object containing 'sandbox' and 'namespace'"
197 .to_string(),
198 ),
199 ));
200 };
201
202 let portal_url = state
204 .get_portal_url_for_sandbox(namespace, sandbox_name)
205 .await?;
206
207 let portal_rpc_url = format!("{}/api/v1/rpc", portal_url);
209
210 debug!("Forwarding RPC to portal: {}", portal_rpc_url);
211
212 let client = reqwest::Client::new();
214
215 const MAX_RETRIES: u32 = 10_000;
217 const TIMEOUT_MS: u64 = 50;
218
219 let mut retry_count = 0;
221 let mut last_error = None;
222
223 while retry_count < MAX_RETRIES {
225 match client
227 .head(&portal_url)
228 .timeout(Duration::from_millis(TIMEOUT_MS))
229 .send()
230 .await
231 {
232 Ok(response) => {
233 debug!(
235 "Successfully connected to portal after {} retries (status: {})",
236 retry_count,
237 response.status()
238 );
239 break;
240 }
241 Err(e) => {
242 last_error = Some(e);
244 trace!("Connection attempt {} failed, retrying...", retry_count + 1);
245 }
246 }
247
248 retry_count += 1;
250 }
251
252 if retry_count >= MAX_RETRIES {
254 let error_msg = if let Some(e) = last_error {
255 format!(
256 "Failed to connect to portal after {} retries: {}",
257 MAX_RETRIES, e
258 )
259 } else {
260 format!("Failed to connect to portal after {} retries", MAX_RETRIES)
261 };
262 return Err(ServerError::InternalError(error_msg));
263 }
264
265 let response = client
267 .post(&portal_rpc_url)
268 .json(&request)
269 .send()
270 .await
271 .map_err(|e| {
272 ServerError::InternalError(format!("Failed to forward RPC to portal: {}", e))
273 })?;
274
275 if !response.status().is_success() {
277 let status = response.status();
278 let error_text = response
279 .text()
280 .await
281 .unwrap_or_else(|_| "Unknown error".to_string());
282
283 return Err(ServerError::InternalError(format!(
284 "Portal returned error status {}: {}",
285 status, error_text
286 )));
287 }
288
289 let portal_response: JsonRpcResponse = response.json().await.map_err(|e| {
291 ServerError::InternalError(format!("Failed to parse portal response: {}", e))
292 })?;
293
294 Ok((StatusCode::OK, Json(portal_response)))
296}
297
298async fn sandbox_start_impl(state: AppState, params: SandboxStartParams) -> ServerResult<String> {
300 validate_sandbox_name(¶ms.sandbox)?;
302 validate_namespace(¶ms.namespace)?;
303
304 let namespace_dir = state
305 .get_config()
306 .get_namespace_dir()
307 .join(¶ms.namespace);
308 let config_file = MICROSANDBOX_CONFIG_FILENAME;
309 let config_path = namespace_dir.join(config_file);
310 let sandbox = ¶ms.sandbox;
311
312 if !namespace_dir.exists() {
314 tokio_fs::create_dir_all(&namespace_dir)
315 .await
316 .map_err(|e| {
317 ServerError::InternalError(format!("Failed to create namespace directory: {}", e))
318 })?;
319
320 menv::initialize(Some(namespace_dir.clone()))
322 .await
323 .map_err(|e| {
324 ServerError::InternalError(format!(
325 "Failed to initialize microsandbox environment: {}",
326 e
327 ))
328 })?;
329 }
330
331 let has_config_in_request = params
333 .config
334 .as_ref()
335 .and_then(|c| c.image.as_ref())
336 .is_some();
337 let has_existing_config_file = config_path.exists();
338
339 if !has_config_in_request && !has_existing_config_file {
340 return Err(ServerError::ValidationError(
341 crate::error::ValidationError::InvalidInput(format!(
342 "No configuration provided and no existing configuration found for sandbox '{}'",
343 sandbox
344 )),
345 ));
346 }
347
348 let mut config_yaml: serde_yaml::Value;
350
351 if has_existing_config_file {
353 let config_content = tokio_fs::read_to_string(&config_path).await.map_err(|e| {
355 ServerError::InternalError(format!("Failed to read config file: {}", e))
356 })?;
357
358 config_yaml = serde_yaml::from_str(&config_content).map_err(|e| {
360 ServerError::InternalError(format!("Failed to parse config file: {}", e))
361 })?;
362
363 if !has_config_in_request {
365 let has_sandbox_config = config_yaml
366 .get("sandboxes")
367 .and_then(|sandboxes| sandboxes.get(sandbox))
368 .is_some();
369
370 if !has_sandbox_config {
371 return Err(ServerError::ValidationError(
372 crate::error::ValidationError::InvalidInput(format!(
373 "Sandbox '{}' not found in existing configuration",
374 sandbox
375 )),
376 ));
377 }
378 }
379 } else {
380 if !has_config_in_request {
382 return Err(ServerError::ValidationError(
383 crate::error::ValidationError::InvalidInput(
384 "No configuration provided and no existing configuration file".to_string(),
385 ),
386 ));
387 }
388
389 tokio_fs::write(&config_path, DEFAULT_CONFIG)
391 .await
392 .map_err(|e| {
393 ServerError::InternalError(format!("Failed to create config file: {}", e))
394 })?;
395
396 config_yaml = serde_yaml::from_str(DEFAULT_CONFIG).map_err(|e| {
398 ServerError::InternalError(format!("Failed to parse default config: {}", e))
399 })?;
400 }
401
402 if !config_yaml.is_mapping() {
404 config_yaml = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
405 }
406
407 let config_map = config_yaml.as_mapping_mut().unwrap();
408 if !config_map.contains_key(&serde_yaml::Value::String("sandboxes".to_string())) {
409 config_map.insert(
410 serde_yaml::Value::String("sandboxes".to_string()),
411 serde_yaml::Value::Mapping(serde_yaml::Mapping::new()),
412 );
413 }
414
415 let sandboxes_key = serde_yaml::Value::String("sandboxes".to_string());
417 let sandboxes_value = config_map.get_mut(&sandboxes_key).unwrap();
418
419 if !sandboxes_value.is_mapping() {
421 *sandboxes_value = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
422 }
423
424 let sandboxes_map = sandboxes_value.as_mapping_mut().unwrap();
425
426 if let Some(config) = ¶ms.config {
428 if config.image.is_some() {
429 let mut sandbox_map = serde_yaml::Mapping::new();
431
432 if let Some(image) = &config.image {
434 sandbox_map.insert(
435 serde_yaml::Value::String("image".to_string()),
436 serde_yaml::Value::String(image.clone()),
437 );
438 }
439
440 if let Some(memory) = config.memory {
442 sandbox_map.insert(
443 serde_yaml::Value::String("memory".to_string()),
444 serde_yaml::Value::Number(serde_yaml::Number::from(memory)),
445 );
446 }
447
448 if let Some(cpus) = config.cpus {
449 sandbox_map.insert(
450 serde_yaml::Value::String("cpus".to_string()),
451 serde_yaml::Value::Number(serde_yaml::Number::from(cpus)),
452 );
453 }
454
455 if !config.volumes.is_empty() {
456 let volumes_array = config
457 .volumes
458 .iter()
459 .map(|v| serde_yaml::Value::String(v.clone()))
460 .collect::<Vec<_>>();
461 sandbox_map.insert(
462 serde_yaml::Value::String("volumes".to_string()),
463 serde_yaml::Value::Sequence(volumes_array),
464 );
465 }
466
467 if !config.ports.is_empty() {
468 let ports_array = config
469 .ports
470 .iter()
471 .map(|p| serde_yaml::Value::String(p.clone()))
472 .collect::<Vec<_>>();
473 sandbox_map.insert(
474 serde_yaml::Value::String("ports".to_string()),
475 serde_yaml::Value::Sequence(ports_array),
476 );
477 }
478
479 if !config.envs.is_empty() {
480 let envs_array = config
481 .envs
482 .iter()
483 .map(|e| serde_yaml::Value::String(e.clone()))
484 .collect::<Vec<_>>();
485 sandbox_map.insert(
486 serde_yaml::Value::String("envs".to_string()),
487 serde_yaml::Value::Sequence(envs_array),
488 );
489 }
490
491 if !config.depends_on.is_empty() {
492 let depends_on_array = config
493 .depends_on
494 .iter()
495 .map(|d| serde_yaml::Value::String(d.clone()))
496 .collect::<Vec<_>>();
497 sandbox_map.insert(
498 serde_yaml::Value::String("depends_on".to_string()),
499 serde_yaml::Value::Sequence(depends_on_array),
500 );
501 }
502
503 if let Some(workdir) = &config.workdir {
504 sandbox_map.insert(
505 serde_yaml::Value::String("workdir".to_string()),
506 serde_yaml::Value::String(workdir.clone()),
507 );
508 }
509
510 if let Some(shell) = &config.shell {
511 sandbox_map.insert(
512 serde_yaml::Value::String("shell".to_string()),
513 serde_yaml::Value::String(shell.clone()),
514 );
515 }
516
517 if !config.scripts.is_empty() {
518 let mut scripts_map = serde_yaml::Mapping::new();
519 for (script_name, script) in &config.scripts {
520 scripts_map.insert(
521 serde_yaml::Value::String(script_name.clone()),
522 serde_yaml::Value::String(script.clone()),
523 );
524 }
525 sandbox_map.insert(
526 serde_yaml::Value::String("scripts".to_string()),
527 serde_yaml::Value::Mapping(scripts_map),
528 );
529 }
530
531 if let Some(exec) = &config.exec {
532 sandbox_map.insert(
533 serde_yaml::Value::String("exec".to_string()),
534 serde_yaml::Value::String(exec.clone()),
535 );
536 }
537
538 sandboxes_map.insert(
540 serde_yaml::Value::String(sandbox.clone()),
541 serde_yaml::Value::Mapping(sandbox_map),
542 );
543 }
544 }
545
546 let sandbox_key = format!("{}/{}", params.namespace, params.sandbox);
548 let port = {
549 let mut port_manager = state.get_port_manager().write().await;
550 port_manager.assign_port(&sandbox_key).await.map_err(|e| {
551 ServerError::InternalError(format!("Failed to assign portal port: {}", e))
552 })?
553 };
554
555 debug!("Assigned portal port {} to sandbox {}", port, sandbox_key);
556
557 let sandbox_config = sandboxes_map
559 .get_mut(&serde_yaml::Value::String(sandbox.clone()))
560 .ok_or_else(|| {
561 ServerError::InternalError(format!("Sandbox '{}' not found in configuration", sandbox))
562 })?
563 .as_mapping_mut()
564 .ok_or_else(|| {
565 ServerError::InternalError(format!(
566 "Sandbox '{}' configuration is not a mapping",
567 sandbox
568 ))
569 })?;
570
571 let guest_port = DEFAULT_PORTAL_GUEST_PORT;
573 let portal_port_mapping = format!("{}:{}", port, guest_port);
574
575 let ports_key = serde_yaml::Value::String("ports".to_string());
576
577 if let Some(ports) = sandbox_config.get_mut(&ports_key) {
578 if let Some(ports_seq) = ports.as_sequence_mut() {
579 ports_seq.retain(|p| {
581 p.as_str()
582 .map(|s| !s.ends_with(&format!(":{}", guest_port)))
583 .unwrap_or(true)
584 });
585
586 ports_seq.push(serde_yaml::Value::String(portal_port_mapping));
588 }
589 } else {
590 let mut ports_seq = serde_yaml::Sequence::new();
592 ports_seq.push(serde_yaml::Value::String(portal_port_mapping));
593 sandbox_config.insert(ports_key, serde_yaml::Value::Sequence(ports_seq));
594 }
595
596 let updated_config = serde_yaml::to_string(&config_yaml)
598 .map_err(|e| ServerError::InternalError(format!("Failed to serialize config: {}", e)))?;
599
600 tokio_fs::write(&config_path, updated_config)
601 .await
602 .map_err(|e| ServerError::InternalError(format!("Failed to write config file: {}", e)))?;
603
604 orchestra::up(
606 vec![sandbox.clone()],
607 Some(&namespace_dir),
608 Some(config_file),
609 true,
610 )
611 .await
612 .map_err(|e| {
613 ServerError::InternalError(format!("Failed to start sandbox {}: {}", params.sandbox, e))
614 })?;
615
616 let potentially_first_time_pull = if let Some(config) = ¶ms.config {
618 config.image.is_some()
619 } else {
620 false
621 };
622
623 let poll_timeout = if potentially_first_time_pull {
626 Duration::from_secs(180) } else {
628 Duration::from_secs(60) };
630
631 debug!("Waiting for sandbox {} to start...", sandbox);
633 match timeout(
634 poll_timeout,
635 poll_sandbox_until_running(¶ms.sandbox, &namespace_dir, config_file),
636 )
637 .await
638 {
639 Ok(result) => match result {
640 Ok(_) => {
641 debug!("Sandbox {} is now running", sandbox);
642 Ok(format!("Sandbox {} started successfully", params.sandbox))
643 }
644 Err(e) => {
645 warn!("Failed to verify sandbox {} is running: {}", sandbox, e);
647 Ok(format!(
648 "Sandbox {} was started, but couldn't verify it's running: {}",
649 params.sandbox, e
650 ))
651 }
652 },
653 Err(_) => {
654 warn!("Timeout waiting for sandbox {} to start", sandbox);
656 Ok(format!(
657 "Sandbox {} was started, but timed out waiting for it to be fully running. It may still be initializing.",
658 params.sandbox
659 ))
660 }
661 }
662}
663
664async fn poll_sandbox_until_running(
666 sandbox_name: &str,
667 namespace_dir: &PathBuf,
668 config_file: &str,
669) -> ServerResult<()> {
670 const POLL_INTERVAL: Duration = Duration::from_millis(20);
671 const MAX_ATTEMPTS: usize = 2500; for attempt in 1..=MAX_ATTEMPTS {
674 let statuses = orchestra::status(
676 vec![sandbox_name.to_string()],
677 Some(namespace_dir),
678 Some(config_file),
679 )
680 .await
681 .map_err(|e| ServerError::InternalError(format!("Failed to get sandbox status: {}", e)))?;
682
683 if let Some(status) = statuses.iter().find(|s| s.name == sandbox_name) {
685 if status.running {
686 debug!(
688 "Sandbox {} is running (verified on attempt {})",
689 sandbox_name, attempt
690 );
691 return Ok(());
692 }
693 }
694
695 sleep(POLL_INTERVAL).await;
697 }
698
699 Err(ServerError::InternalError(format!(
701 "Exceeded maximum attempts to verify sandbox {} is running",
702 sandbox_name
703 )))
704}
705
706async fn sandbox_stop_impl(state: AppState, params: SandboxStopParams) -> ServerResult<String> {
708 validate_sandbox_name(¶ms.sandbox)?;
710 validate_namespace(¶ms.namespace)?;
711
712 let namespace_dir = state
713 .get_config()
714 .get_namespace_dir()
715 .join(¶ms.namespace);
716 let config_file = MICROSANDBOX_CONFIG_FILENAME;
717 let sandbox = ¶ms.sandbox;
718 let sandbox_key = format!("{}/{}", params.namespace, params.sandbox);
719
720 if !namespace_dir.exists() {
722 return Err(ServerError::ValidationError(
723 crate::error::ValidationError::InvalidInput(format!(
724 "Namespace directory '{}' does not exist",
725 params.namespace
726 )),
727 ));
728 }
729
730 let config_path = namespace_dir.join(config_file);
732 if !config_path.exists() {
733 return Err(ServerError::ValidationError(
734 crate::error::ValidationError::InvalidInput(format!(
735 "Configuration file not found for namespace '{}'",
736 params.namespace
737 )),
738 ));
739 }
740
741 orchestra::down(
743 vec![sandbox.clone()],
744 Some(&namespace_dir),
745 Some(config_file),
746 )
747 .await
748 .map_err(|e| {
749 ServerError::InternalError(format!("Failed to stop sandbox {}: {}", params.sandbox, e))
750 })?;
751
752 {
754 let mut port_manager = state.get_port_manager().write().await;
755 port_manager.release_port(&sandbox_key).await.map_err(|e| {
756 ServerError::InternalError(format!("Failed to release portal port: {}", e))
757 })?;
758 }
759
760 debug!("Released portal port for sandbox {}", sandbox_key);
761
762 Ok(format!("Sandbox {} stopped successfully", params.sandbox))
764}
765
766async fn sandbox_get_metrics_impl(
768 state: AppState,
769 params: SandboxMetricsGetParams,
770) -> ServerResult<SandboxStatusResponse> {
771 if params.namespace != "*" {
773 validate_namespace(¶ms.namespace)?;
774 }
775
776 if let Some(sandbox) = ¶ms.sandbox {
778 validate_sandbox_name(sandbox)?;
779 }
780
781 let namespaces_dir = state.get_config().get_namespace_dir();
782
783 if !namespaces_dir.exists() {
785 return Err(ServerError::InternalError(format!(
786 "Namespaces directory '{}' does not exist",
787 namespaces_dir.display()
788 )));
789 }
790
791 let mut all_statuses = Vec::new();
793
794 if params.namespace == "*" {
796 let mut entries = tokio::fs::read_dir(&namespaces_dir).await.map_err(|e| {
798 ServerError::InternalError(format!("Failed to read namespaces directory: {}", e))
799 })?;
800
801 while let Some(entry) = entries.next_entry().await.map_err(|e| {
803 ServerError::InternalError(format!("Failed to read namespace directory entry: {}", e))
804 })? {
805 let path = entry.path();
806 if !path.is_dir() {
807 continue;
808 }
809
810 let namespace = path
811 .file_name()
812 .and_then(|n| n.to_str())
813 .unwrap_or("unknown")
814 .to_string();
815
816 let sandbox_names = if let Some(sandbox) = ¶ms.sandbox {
818 vec![sandbox.clone()]
819 } else {
820 vec![]
821 };
822
823 match orchestra::status(sandbox_names, Some(&path), None).await {
824 Ok(statuses) => {
825 for status in statuses {
826 all_statuses.push(SandboxStatus {
828 namespace: namespace.clone(),
829 name: status.name,
830 running: status.running,
831 cpu_usage: status.cpu_usage,
832 memory_usage: status.memory_usage,
833 disk_usage: status.disk_usage,
834 });
835 }
836 }
837 Err(e) => {
838 tracing::warn!("Error getting metrics for namespace {}: {}", namespace, e);
840 }
841 }
842 }
843 } else {
844 let namespace_dir = namespaces_dir.join(¶ms.namespace);
846
847 if !namespace_dir.exists() {
849 return Err(ServerError::ValidationError(
850 crate::error::ValidationError::InvalidInput(format!(
851 "Namespace directory '{}' does not exist",
852 params.namespace
853 )),
854 ));
855 }
856
857 let sandbox_names = if let Some(sandbox) = ¶ms.sandbox {
859 vec![sandbox.clone()]
860 } else {
861 vec![]
862 };
863
864 match orchestra::status(sandbox_names, Some(&namespace_dir), None).await {
865 Ok(statuses) => {
866 for status in statuses {
867 all_statuses.push(SandboxStatus {
869 namespace: params.namespace.clone(),
870 name: status.name,
871 running: status.running,
872 cpu_usage: status.cpu_usage,
873 memory_usage: status.memory_usage,
874 disk_usage: status.disk_usage,
875 });
876 }
877 }
878 Err(e) => {
879 return Err(ServerError::InternalError(format!(
880 "Error getting metrics for namespace {}: {}",
881 params.namespace, e
882 )));
883 }
884 }
885 }
886
887 Ok(SandboxStatusResponse {
888 sandboxes: all_statuses,
889 })
890}
891
892pub async fn proxy_request(
898 State(_state): State<AppState>,
899 Path((namespace, sandbox, path)): Path<(String, String, PathBuf)>,
900 req: Request<Body>,
901) -> ServerResult<impl IntoResponse> {
902 let path_str = path.display().to_string();
906
907 let original_uri = req.uri().clone();
909 let _target_uri = middleware::proxy_uri(original_uri, &namespace, &sandbox);
910
911 let response = format!(
915 "Axum Proxy Request\n\nNamespace: {}\nSandbox: {}\nPath: {}\nMethod: {}\nHeaders: {:?}",
916 namespace,
917 sandbox,
918 path_str,
919 req.method(),
920 req.headers()
921 );
922
923 let result = Response::builder()
924 .status(StatusCode::OK)
925 .header("Content-Type", "text/plain")
926 .body(Body::from(response))
927 .unwrap();
928
929 Ok(result)
930}
931
932pub async fn proxy_fallback() -> ServerResult<impl IntoResponse> {
934 Ok((StatusCode::NOT_FOUND, "Resource not found"))
935}
936
937fn validate_sandbox_name(name: &str) -> ServerResult<()> {
943 if name.is_empty() {
945 return Err(ServerError::ValidationError(
946 crate::error::ValidationError::InvalidInput("Sandbox name cannot be empty".to_string()),
947 ));
948 }
949
950 if name.len() > 63 {
951 return Err(ServerError::ValidationError(
952 crate::error::ValidationError::InvalidInput(
953 "Sandbox name cannot exceed 63 characters".to_string(),
954 ),
955 ));
956 }
957
958 let valid_chars = name
960 .chars()
961 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
962
963 if !valid_chars {
964 return Err(ServerError::ValidationError(
965 crate::error::ValidationError::InvalidInput(
966 "Sandbox name can only contain alphanumeric characters, hyphens, or underscores"
967 .to_string(),
968 ),
969 ));
970 }
971
972 if !name.chars().next().unwrap().is_ascii_alphanumeric() {
974 return Err(ServerError::ValidationError(
975 crate::error::ValidationError::InvalidInput(
976 "Sandbox name must start with an alphanumeric character".to_string(),
977 ),
978 ));
979 }
980
981 Ok(())
982}
983
984fn validate_namespace(namespace: &str) -> ServerResult<()> {
986 if namespace.is_empty() {
988 return Err(ServerError::ValidationError(
989 crate::error::ValidationError::InvalidInput("Namespace cannot be empty".to_string()),
990 ));
991 }
992
993 if namespace.len() > 63 {
994 return Err(ServerError::ValidationError(
995 crate::error::ValidationError::InvalidInput(
996 "Namespace cannot exceed 63 characters".to_string(),
997 ),
998 ));
999 }
1000
1001 if namespace == "*" {
1003 return Err(ServerError::ValidationError(
1004 crate::error::ValidationError::InvalidInput(
1005 "Wildcard namespace (*) is not valid for sandbox creation".to_string(),
1006 ),
1007 ));
1008 }
1009
1010 let valid_chars = namespace
1012 .chars()
1013 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
1014
1015 if !valid_chars {
1016 return Err(ServerError::ValidationError(
1017 crate::error::ValidationError::InvalidInput(
1018 "Namespace can only contain alphanumeric characters, hyphens, or underscores"
1019 .to_string(),
1020 ),
1021 ));
1022 }
1023
1024 if !namespace.chars().next().unwrap().is_ascii_alphanumeric() {
1026 return Err(ServerError::ValidationError(
1027 crate::error::ValidationError::InvalidInput(
1028 "Namespace must start with an alphanumeric character".to_string(),
1029 ),
1030 ));
1031 }
1032
1033 Ok(())
1034}