wasm-sandbox 0.4.1

A secure WebAssembly sandbox with dead-simple ease of use, progressive complexity APIs, and comprehensive safety controls
Documentation
//! HTTP Server template constants

/// Template for HTTP server wrapper
pub const HTTP_SERVER_TEMPLATE: &str = r#"// Generated HTTP Server WASM wrapper
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::Duration;

use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use serde::{Serialize, Deserialize};
use once_cell::sync::Lazy;
use anyhow::{Result, anyhow, Context};

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use hyper::server::conn::AddrIncoming;
use http::{HeaderMap, HeaderValue, StatusCode};
use std::convert::Infallible;
use std::net::SocketAddr;
use tokio::sync::oneshot;
use futures::channel::mpsc::{unbounded, UnboundedSender, UnboundedReceiver};
use futures::StreamExt;

// Configuration
const APP_PATH: &str = "{{ app_path }}";
const PORT: u16 = {{ port }};
const HOST: &str = "{{ host }}";
const MAX_BODY_SIZE: usize = {{ max_body_size }};
const REQUEST_TIMEOUT: u64 = {{ request_timeout }};
const CORS_ENABLED: bool = {{ cors_enabled }};

// Global state
static SERVER: Lazy<Mutex<Option<ServerState>>> = Lazy::new(|| Mutex::new(None));
static RUNNING: AtomicBool = AtomicBool::new(false);

// Arguments
const APP_ARGS: &[&str] = &[
    {{ app_args }}
];

// Server state
struct ServerState {
    port: u16,
    shutdown_tx: Option<oneshot::Sender<()>>,
}

// Request handler
struct RequestHandler {
    request_tx: UnboundedSender<HttpExchange>,
    response_rx_map: Arc<Mutex<HashMap<String, oneshot::Receiver<HttpResponse>>>>,
}

// HTTP exchange
struct HttpExchange {
    id: String,
    request: HttpRequest,
    response_tx: oneshot::Sender<HttpResponse>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct HttpRequest {
    method: String,
    path: String,
    query: Option<String>,
    headers: HashMap<String, String>,
    body: Option<Vec<u8>>,
    remote: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct HttpResponse {
    status: u16,
    headers: HashMap<String, String>,
    body: Vec<u8>,
}

impl RequestHandler {
    fn new() -> (Self, UnboundedReceiver<HttpExchange>) {
        let (tx, rx) = unbounded();
        let handler = RequestHandler {
            request_tx: tx,
            response_rx_map: Arc::new(Mutex::new(HashMap::new())),
        };
        (handler, rx)
    }

    async fn handle_request(&self, req: Request<Body>) -> Result<Response<Body>> {
        // Generate a unique ID for this request
        let id = uuid::Uuid::new_v4().to_string();
        
        // Convert Hyper request to our HttpRequest
        let mut headers = HashMap::new();
        for (name, value) in req.headers() {
            if let Ok(v) = value.to_str() {
                headers.insert(name.to_string(), v.to_string());
            }
        }
        
        let method = req.method().to_string();
        let uri = req.uri();
        let path = uri.path().to_string();
        let query = uri.query().map(|q| q.to_string());
        
        let remote = req.extensions()
            .get::<hyper::server::conn::RemoteAddr>()
            .map(|addr| addr.to_string());
        
        // Read the body
        let body_bytes = hyper::body::to_bytes(req.into_body())
            .await
            .context("Failed to read request body")?;
            
        let body = if !body_bytes.is_empty() {
            Some(body_bytes.to_vec())
        } else {
            None
        };
        
        // Create HttpRequest
        let http_request = HttpRequest {
            method,
            path,
            query,
            headers,
            body,
            remote,
        };
        
        // Create a channel for the response
        let (response_tx, response_rx) = oneshot::channel();
        
        // Store the response receiver
        self.response_rx_map.lock().unwrap().insert(id.clone(), response_rx);
        
        // Send the request to the handler
        self.request_tx.unbounded_send(HttpExchange {
            id: id.clone(),
            request: http_request,
            response_tx,
        }).context("Failed to send request to handler")?;
        
        // Wait for the response
        let response = tokio::time::timeout(
            Duration::from_secs(REQUEST_TIMEOUT),
            async {
                let mut map = self.response_rx_map.lock().unwrap();
                if let Some(rx) = map.remove(&id) {
                    rx.await.context("Failed to receive response")
                } else {
                    Err(anyhow!("Response receiver not found"))
                }
            }
        ).await??;
        
        // Convert to Hyper response
        let mut builder = Response::builder()
            .status(StatusCode::from_u16(response.status)?);
            
        for (name, value) in response.headers {
            builder = builder.header(&name, &value);
        }
        
        if CORS_ENABLED {
            builder = builder
                .header("Access-Control-Allow-Origin", "{{ cors_allowed_origins }}")
                .header("Access-Control-Allow-Methods", "{{ cors_allowed_methods }}")
                .header("Access-Control-Allow-Headers", "{{ cors_allowed_headers }}");
        }
        
        let hyper_response = builder
            .body(Body::from(response.body))
            .context("Failed to build response")?;
            
        Ok(hyper_response)
    }
}

// Start the server
#[wasm_bindgen]
pub fn start(port: u16) -> u16 {
    console_error_panic_hook::set_once();
    
    // Check if already running
    if RUNNING.load(Ordering::SeqCst) {
        let server = SERVER.lock().unwrap();
        return server.as_ref().map_or(0, |s| s.port);
    }
    
    let actual_port = if port > 0 { port } else { PORT };
    
    // Create the runtime
    let runtime = match tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build() {
        Ok(rt) => rt,
        Err(_) => return 0,
    };
    
    // Create the request handler
    let (handler, mut rx) = RequestHandler::new();
    let handler = Arc::new(handler);
    
    // Create server
    let addr = format!("{}:{}", HOST, actual_port).parse::<SocketAddr>().unwrap();
    
    let handler_clone = handler.clone();
    let make_service = make_service_fn(move |_conn| {
        let handler = handler_clone.clone();
        async move {
            Ok::<_, Infallible>(service_fn(move |req| {
                let handler = handler.clone();
                async move {
                    match handler.handle_request(req).await {
                        Ok(resp) => Ok(resp),
                        Err(_) => {
                            let mut resp = Response::new(Body::from("Internal Server Error"));
                            *resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
                            Ok(resp)
                        }
                    }
                }
            }))
        }
    });
    
    // Start the server
    let server = Server::bind(&addr)
        .serve(make_service);
    
    let (tx, rx) = oneshot::channel::<()>();
    let graceful = server.with_graceful_shutdown(async {
        rx.await.ok();
    });
    
    // Spawn the server task
    runtime.spawn(async move {
        if let Err(e) = graceful.await {
            eprintln!("Server error: {}", e);
        }
        RUNNING.store(false, Ordering::SeqCst);
    });
    
    // Spawn the request handler task
    runtime.spawn(async move {
        while let Some(exchange) = rx.next().await {
            // Process the request and send response
            let response = handle_app_request(&exchange.request).await;
            let _ = exchange.response_tx.send(response);
        }
    });
    
    // Update server state
    let mut server_state = SERVER.lock().unwrap();
    *server_state = Some(ServerState {
        port: actual_port,
        shutdown_tx: Some(tx),
    });
    
    RUNNING.store(true, Ordering::SeqCst);
    actual_port
}

// Stop the server
#[wasm_bindgen]
pub fn stop() {
    let mut server = SERVER.lock().unwrap();
    if let Some(state) = server.as_mut() {
        if let Some(tx) = state.shutdown_tx.take() {
            let _ = tx.send(());
        }
    }
    
    RUNNING.store(false, Ordering::SeqCst);
}

// Check if server is running
#[wasm_bindgen]
pub fn is_running() -> bool {
    RUNNING.load(Ordering::SeqCst)
}

// Get server port
#[wasm_bindgen]
pub fn get_port() -> u16 {
    let server = SERVER.lock().unwrap();
    server.as_ref().map_or(0, |s| s.port)
}

// Handle requests by forwarding to the application
async fn handle_app_request(request: &HttpRequest) -> HttpResponse {
    // In a real implementation, we would communicate with the actual application process
    // For now, we return a simple response
    HttpResponse {
        status: 200,
        headers: HashMap::from([
            ("Content-Type".to_string(), "text/plain".to_string()),
        ]),
        body: format!("Request to {} handled by WASM sandbox", request.path).into_bytes(),
    }
}
"#;