jsandbox 0.7.0

JavaScript sandbox using deno runtime.
Documentation
use async_trait::async_trait;
use deno_core::*;
use deno_runtime::deno_permissions::PermissionsContainer;
use deno_runtime::worker::{MainWorker, WorkerOptions};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
use std::{thread, time::Duration};

use crate::args::Args;

pub type AnyError = error::AnyError;
pub type Permissions = deno_runtime::deno_permissions::Permissions;
pub type PermissionsOptions = deno_runtime::deno_permissions::PermissionsOptions;

#[async_trait]
pub trait Callback {
    async fn callback(&mut self, value: serde_json::Value) -> Result<serde_json::Value, AnyError>;
}

pub struct Script {
    code: String,
    callback: CallbackFunctions,
    timeout: Duration,
    permissions: Permissions,
    worker: Option<MainWorker>,
}

#[derive(Clone)]
struct CallbackFunctions {
    functions: HashMap<String, Arc<Mutex<dyn Callback>>>,
}

impl Resource for CallbackFunctions {
    fn name(&self) -> std::borrow::Cow<str> {
        "CallbackFunctions".into()
    }
}

struct ReturnValue {
    value: serde_json::Value,
}

impl Resource for ReturnValue {
    fn name(&self) -> std::borrow::Cow<str> {
        "ReturnValue".into()
    }
}

#[op2(async)]
#[serde]
async fn op_callback(
    state: Rc<RefCell<OpState>>,
    rid: u32,
    #[string] name: String,
    #[serde] value: serde_json::Value,
) -> Result<serde_json::Value, AnyError> {
    let callbacks = match state.borrow().resource_table.get::<CallbackFunctions>(rid) {
        Ok(v) => v,
        Err(..) => return Err(error::generic_error("No callbacks found in resource table")),
    };

    let callback = match callbacks.functions.get(&name) {
        Some(v) => v,
        None => return Err(error::generic_error("No callback function found")),
    };

    let mut x = callback.lock().unwrap();
    x.callback(value).await
}

#[op2]
fn op_return(state: Rc<RefCell<OpState>>, #[serde] args: serde_json::Value) {
    state
        .borrow_mut()
        .resource_table
        .add(ReturnValue { value: args });
}

deno_core::extension!(script_runtime, ops = [op_return, op_callback]);

impl Script {
    pub fn from_string(code: &str) -> Self {
        Script {
            code: code.into(),
            callback: CallbackFunctions {
                functions: HashMap::new(),
            },
            timeout: Duration::ZERO,
            permissions: Permissions::none_without_prompt(),
            worker: None,
        }
    }

    pub fn function(mut self, name: String, function: impl Callback + 'static) -> Self {
        self.callback
            .functions
            .insert(name, Arc::new(Mutex::new(function)));
        self
    }

    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    pub fn permissions(mut self, permissions: Permissions) -> Self {
        self.permissions = permissions;
        self
    }

    pub fn build(mut self) -> Result<Self, error::AnyError> {
        self.worker = Some(MainWorker::bootstrap_from_options(
            url::Url::parse("data:text/plain,").unwrap(),
            PermissionsContainer::new(self.permissions.clone()),
            WorkerOptions {
                extensions: vec![script_runtime::init_ops()],
                ..Default::default()
            },
        ));

        let worker = self.worker.as_mut().unwrap();
        worker.execute_script(
            "ext:<anon>",
            format!(
                r#"
                //var op_callback, op_return;
                import("ext:core/ops").then((imported) => {{
                    globalThis.op_callback = imported.op_callback;
                    globalThis.op_return = imported.op_return;
                }});
                "#
            )
            .into(),
        )?;

        let state_rc = worker.js_runtime.op_state();
        let mut state = state_rc.borrow_mut();
        let callback_rid = state.resource_table.add(self.callback.clone());
        for function in &self.callback.functions {
            let func_name = function.0;

            worker.execute_script(
                "ext:<anon>",
                format!(
                    r#"
                    function {func_name} (value) {{
                        return /*Deno[Deno.internal].core.ops.*/op_callback({callback_rid}, '{func_name}', value);
                    }}
                    "#
                )
                .into(),
            )?;
        }

        worker.execute_script("ext:<anon>", self.code.clone().into())?;

        Ok(self)
    }

    pub async fn call<T, R>(&mut self, func: &str, args: T) -> Result<R, error::AnyError>
    where
        T: Args,
        R: serde::de::DeserializeOwned,
    {
        if self.worker.is_none() {
            return Err(error::AnyError::msg(
                "Script is not properly initialized, did you call build()?",
            ));
        }

        let worker = self.worker.as_mut().unwrap();

        worker
            .js_runtime
            .run_event_loop(PollEventLoopOptions::default())
            .await?;

        let a = args.to_string()?;

        if self.timeout > Duration::ZERO {
            let handle = worker.js_runtime.v8_isolate().thread_safe_handle();
            let timeout = self.timeout;
            thread::spawn(move || {
                thread::sleep(timeout);
                handle.terminate_execution();
            });
        }

        worker.execute_script(
            "ext:<anon>",
            format!(
                r#"
                (async () => {{
                    /*Deno[Deno.internal].core.ops.*/op_return(
                        {func}.constructor.name === 'AsyncFunction' ? await {func}({a}) : {func}({a})
                    );
                }})();
                "#
            ).into(),
        )?;

        worker
            .js_runtime
            .run_event_loop(PollEventLoopOptions::default())
            .await?;

        let state_rc = worker.js_runtime.op_state();
        let mut state = state_rc.borrow_mut();
        let result: std::rc::Rc<ReturnValue>;
        let mut rid: u32 = 0;
        for val in state.resource_table.names() {
            if val.1 == "ReturnValue" {
                rid = val.0;
            }
        }
        result = state.resource_table.take::<ReturnValue>(rid).unwrap();

        Ok(serde_json::from_value(result.value.clone()).unwrap())
    }
}

pub async fn eval<R>(expr: &str) -> Result<R, error::AnyError>
where
    R: serde::de::DeserializeOwned,
{
    let mut script = Script::from_string(&format!(
        r#"
        function expr() {{
            return {expr};
        }}
        "#
    ))
    .build()?;
    script.call("expr", ()).await
}