From 9366dbb96e82c9744eea3eaca88484968e9e892d Mon Sep 17 00:00:00 2001 From: BluJ Date: Mon, 6 Feb 2023 12:14:48 -0700 Subject: [PATCH] wip: Starting down the bind for the effects todo: complete a ip todo chore: Fix the result type on something todo: Address returning chore: JS with callbacks chore: Add in the chown and permissions chore: Add in the binds and unbinds in --- backend/src/manager/js_api.rs | 111 +- backend/src/manager/manager_container.rs | 8 +- backend/src/manager/mod.rs | 14 +- backend/src/procedure/js_scripts.rs | 949 ++++++++++++------ .../scripts/test-package/0.3.0.3/embassy.js | 90 +- .../data/main/pem-chown/deep/123/test.txt | 1 + libs/helpers/src/os_api.rs | 69 +- libs/helpers/src/rsync.rs | 3 + libs/js_engine/src/artifacts/loadModule.js | 260 +++-- libs/js_engine/src/lib.rs | 192 +++- 10 files changed, 1237 insertions(+), 460 deletions(-) create mode 100644 backend/test/js_action_execute/package-data/volumes/test-package/data/main/pem-chown/deep/123/test.txt diff --git a/backend/src/manager/js_api.rs b/backend/src/manager/js_api.rs index 3e782e0d0..c924e7ec2 100644 --- a/backend/src/manager/js_api.rs +++ b/backend/src/manager/js_api.rs @@ -1,8 +1,11 @@ -use helpers::{Callback, OsApi}; -use models::PackageId; +use color_eyre::{eyre::eyre, Report}; +use helpers::{AddressSchemaLocal, AddressSchemaOnion, Callback, OsApi}; +use models::{InterfaceId, PackageId}; +use sqlx::Acquire; -use crate::manager::Manager; -use crate::Error; +use crate::{manager::Manager, net::keys::Key}; + +use super::try_get_running_ip; #[async_trait::async_trait] impl OsApi for Manager { @@ -11,7 +14,103 @@ impl OsApi for Manager { id: PackageId, path: &str, callback: Callback, - ) -> Result { - todo!() + ) -> Result { + todo!("BLUJ") + } + async fn bind_local( + &self, + internal_port: u16, + address_schema: AddressSchemaLocal, + ) -> Result { + let ip = try_get_running_ip(&self.seed) + .await? + .ok_or_else(|| eyre!("No ip available"))?; + let AddressSchemaLocal { id, external_port } = address_schema; + let mut svc = self + .seed + .ctx + .net_controller + .create_service(self.seed.manifest.id.clone(), ip) + .await + .map_err(|e| eyre!("Could not get to net controller: {e:?}"))?; + let mut secrets = self.seed.ctx.secret_store.acquire().await?; + let mut tx = secrets.begin().await?; + + svc.add_lan(&mut tx, id.clone(), external_port, internal_port, false) + .await + .map_err(|e| eyre!("Could not add to local: {e:?}"))?; + let key = Key::for_interface(&mut tx, Some((self.seed.manifest.id.clone(), id))) + .await + .map_err(|e| eyre!("Could not get network name: {e:?}"))? + .local_address(); + + tx.commit().await?; + Ok(helpers::Address(key)) + } + async fn bind_onion( + &self, + internal_port: u16, + address_schema: AddressSchemaOnion, + ) -> Result { + let AddressSchemaOnion { id, external_port } = address_schema; + let ip = try_get_running_ip(&self.seed) + .await? + .ok_or_else(|| eyre!("No ip available"))?; + let mut svc = self + .seed + .ctx + .net_controller + .create_service(self.seed.manifest.id.clone(), ip) + .await + .map_err(|e| eyre!("Could not get to net controller: {e:?}"))?; + let mut secrets = self.seed.ctx.secret_store.acquire().await?; + let mut tx = secrets.begin().await?; + + svc.add_tor(&mut tx, id.clone(), external_port, internal_port) + .await + .map_err(|e| eyre!("Could not add to tor: {e:?}"))?; + let key = Key::for_interface(&mut tx, Some((self.seed.manifest.id.clone(), id))) + .await + .map_err(|e| eyre!("Could not get network name: {e:?}"))? + .tor_address() + .to_string(); + tx.commit().await?; + Ok(helpers::Address(key)) + } + async fn unbind_onion(&self, id: InterfaceId, external: u16) -> Result<(), Report> { + let ip = try_get_running_ip(&self.seed) + .await? + .ok_or_else(|| eyre!("No ip available"))?; + let mut svc = self + .seed + .ctx + .net_controller + .create_service(self.seed.manifest.id.clone(), ip) + .await + .map_err(|e| eyre!("Could not get to net controller: {e:?}"))?; + let mut secrets = self.seed.ctx.secret_store.acquire().await?; + + svc.remove_tor(id, external) + .await + .map_err(|e| eyre!("Could not add to tor: {e:?}"))?; + Ok(()) + } + async fn unbind_local(&self, id: InterfaceId, external: u16) -> Result<(), Report> { + let ip = try_get_running_ip(&self.seed) + .await? + .ok_or_else(|| eyre!("No ip available"))?; + let mut svc = self + .seed + .ctx + .net_controller + .create_service(self.seed.manifest.id.clone(), ip) + .await + .map_err(|e| eyre!("Could not get to net controller: {e:?}"))?; + let mut secrets = self.seed.ctx.secret_store.acquire().await?; + + svc.remove_lan(id, external) + .await + .map_err(|e| eyre!("Could not add to local: {e:?}"))?; + Ok(()) } } diff --git a/backend/src/manager/manager_container.rs b/backend/src/manager/manager_container.rs index 6c8b4db7f..74eb73ef3 100644 --- a/backend/src/manager/manager_container.rs +++ b/backend/src/manager/manager_container.rs @@ -251,7 +251,10 @@ async fn run_main_log_result(result: RunMainResult, seed: Arc Result { +pub(super) async fn get_status( + db: &mut PatchDbHandle, + manifest: &Manifest, +) -> Result { Ok(crate::db::DatabaseModel::new() .package_data() .idx_model(&manifest.id) @@ -283,7 +286,6 @@ async fn set_status( .status() .main() .put(db, main_status) - .await? - .clone(); + .await?; Ok(()) } diff --git a/backend/src/manager/mod.rs b/backend/src/manager/mod.rs index 215006cad..262dea5e0 100644 --- a/backend/src/manager/mod.rs +++ b/backend/src/manager/mod.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::task::Poll; use std::time::Duration; -use color_eyre::eyre::eyre; +use color_eyre::{eyre::eyre, Report}; use embassy_container_init::ProcessGroupId; use futures::future::BoxFuture; use futures::{FutureExt, TryFutureExt}; @@ -833,6 +833,18 @@ async fn main_health_check_daemon(seed: Arc) { type RuntimeOfCommand = NonDetachingJoinHandle, Error>>; +async fn try_get_running_ip(seed: &ManagerSeed) -> Result, Report> { + Ok(container_inspect(seed) + .await + .map(|x| x.network_settings)? + .and_then(|ns| ns.networks) + .and_then(|mut n| n.remove("start9")) + .and_then(|es| es.ip_address) + .filter(|ip| !ip.is_empty()) + .map(|ip| ip.parse()) + .transpose()?) +} + async fn get_running_ip(seed: &ManagerSeed, mut runtime: &mut RuntimeOfCommand) -> GetRunningIp { loop { match container_inspect(seed).await { diff --git a/backend/src/procedure/js_scripts.rs b/backend/src/procedure/js_scripts.rs index bf6820b5c..ca1d2eb75 100644 --- a/backend/src/procedure/js_scripts.rs +++ b/backend/src/procedure/js_scripts.rs @@ -3,8 +3,9 @@ use std::sync::Arc; use std::time::Duration; use color_eyre::eyre::eyre; +use color_eyre::Report; use embassy_container_init::{ProcessGroupId, SignalGroup, SignalGroupParams}; -use helpers::{OsApi, UnixRpcClient}; +use helpers::{Address, AddressSchemaLocal, AddressSchemaOnion, Callback, OsApi, UnixRpcClient}; pub use js_engine::JsError; use js_engine::{JsExecutionEnvironment, PathForVolumeId}; use models::{ErrorKind, VolumeId}; @@ -49,7 +50,33 @@ struct SandboxOsApi { _ctx: RpcContext, } #[async_trait::async_trait] -impl OsApi for SandboxOsApi {} +impl OsApi for SandboxOsApi { + #[allow(unused_variables)] + async fn get_service_config( + &self, + id: PackageId, + path: &str, + callback: Callback, + ) -> Result { + todo!() + } + #[allow(unused_variables)] + async fn bind_local( + &self, + internal_port: u16, + address_schema: AddressSchemaLocal, + ) -> Result { + todo!() + } + #[allow(unused_variables)] + async fn bind_onion( + &self, + internal_port: u16, + address_schema: AddressSchemaOnion, + ) -> Result { + todo!() + } +} #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] @@ -181,190 +208,189 @@ fn unwrap_known_error( } } -#[tokio::test] -async fn js_action_execute() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::GetConfig; - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = Some(serde_json::json!({"test":123})); - let timeout = Some(Duration::from_secs(10)); - let _output: crate::config::action::ConfigRes = js_action - .execute( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - None, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - &std::fs::read_to_string( - "test/js_action_execute/package-data/volumes/test-package/data/main/test.log" - ) - .unwrap(), - "This is a test" - ); - std::fs::remove_file( - "test/js_action_execute/package-data/volumes/test-package/data/main/test.log", - ) - .unwrap(); -} +#[cfg(test)] +mod tests { + use super::*; + use helpers::{Address, AddressSchemaLocal, AddressSchemaOnion, Callback, OsApi}; + use serde_json::{json, Value}; + use tokio::sync::watch; -#[tokio::test] -async fn js_action_execute_error() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::SetConfig; - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", + struct OsApiMock { + config_callbacks: watch::Sender>, + } + impl Default for OsApiMock { + fn default() -> Self { + Self { + config_callbacks: watch::channel(Vec::new()).0, + } } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - let output: Result = js_action - .execute( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - None, - ) - .await - .unwrap(); - assert_eq!("Err((2, \"Not setup\"))", &format!("{:?}", output)); -} + } -#[tokio::test] -async fn js_action_fetch() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("fetch".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", + #[async_trait::async_trait] + impl OsApi for OsApiMock { + #[allow(unused_variables)] + async fn get_service_config( + &self, + id: PackageId, + path: &str, + callback: Callback, + ) -> Result { + println!("Adding callback"); + self.config_callbacks.send_modify(|x| x.push(callback)); + Ok(Value::Null) } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - None, + #[allow(unused_variables)] + async fn bind_local( + &self, + internal_port: u16, + address_schema: AddressSchemaLocal, + ) -> Result { + todo!() + } + #[allow(unused_variables)] + async fn bind_onion( + &self, + internal_port: u16, + address_schema: AddressSchemaOnion, + ) -> Result { + todo!() + } + } + #[tokio::test] + async fn js_action_execute() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::GetConfig; + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) + .unwrap(); + let input: Option = Some(json!({"test":123})); + let timeout = Some(Duration::from_secs(10)); + let _output: crate::config::action::ConfigRes = js_action + .execute( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + Arc::new(OsApiMock::default()), + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + &std::fs::read_to_string( + "test/js_action_execute/package-data/volumes/test-package/data/main/test.log" + ) + .unwrap(), + "This is a test" + ); + std::fs::remove_file( + "test/js_action_execute/package-data/volumes/test-package/data/main/test.log", ) - .await - .unwrap() .unwrap(); -} + } -#[tokio::test] -async fn js_test_slow() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() + #[tokio::test] + async fn js_action_execute_error() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::SetConfig; + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("slow".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - tracing::debug!("testing start"); - tokio::select! { - a = js_action + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + let output: Result = js_action + .execute( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + Arc::new(OsApiMock::default()), + ) + .await + .unwrap(); + assert_eq!("Err((2, \"Not setup\"))", &format!("{:?}", output)); + } + + #[tokio::test] + async fn js_action_fetch() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("fetch".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) + .unwrap(); + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action .execute::( &path, &package_id, @@ -375,117 +401,138 @@ async fn js_test_slow() { timeout, ProcessGroupId(0), None, - None, - ) => { a.unwrap().unwrap(); }, - _ = tokio::time::sleep(Duration::from_secs(1)) => () + Arc::new(OsApiMock::default()), + ) + .await + .unwrap() + .unwrap(); } - tracing::debug!("testing end should"); - tokio::time::sleep(Duration::from_secs(2)).await; - tracing::debug!("Done"); -} -#[tokio::test] -async fn js_action_var_arg() { - let js_action = JsProcedure { - args: vec![42.into()], - }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("js-action-var-arg".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - None, - ) - .await - .unwrap() - .unwrap(); -} -#[tokio::test] -async fn js_action_test_rename() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() + #[tokio::test] + async fn js_test_slow() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("slow".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-rename".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + tracing::debug!("testing start"); + tokio::select! { + a = js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + Arc::new(OsApiMock::default()) + ) => { a.unwrap().unwrap(); }, + _ = tokio::time::sleep(Duration::from_secs(1)) => () } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - None, - ) - .await - .unwrap() + tracing::debug!("testing end should"); + tokio::time::sleep(Duration::from_secs(2)).await; + tracing::debug!("Done"); + } + #[tokio::test] + async fn js_action_var_arg() { + let js_action = JsProcedure { + args: vec![42.into()], + }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("js-action-var-arg".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); -} + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + Arc::new(OsApiMock::default()), + ) + .await + .unwrap() + .unwrap(); + } -#[tokio::test] -async fn js_action_test_deep_dir() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() + #[tokio::test] + async fn js_action_test_rename() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-rename".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); let package_id = "test-package".parse().unwrap(); let package_version: Version = "0.3.0.3".parse().unwrap(); @@ -663,51 +710,301 @@ async fn js_action_test_read_dir() { .unwrap(); } -#[tokio::test] -async fn js_rsync() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() + #[tokio::test] + async fn js_action_test_deep_dir() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-deep-dir".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-rsync".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - None, - ) - .await - .unwrap() + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + Arc::new(OsApiMock::default()), + ) + .await + .unwrap() + .unwrap(); + } + #[tokio::test] + async fn js_action_test_deep_dir_escape() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-deep-dir-escape".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + Arc::new(OsApiMock::default()), + ) + .await + .unwrap() + .unwrap(); + } + #[tokio::test] + async fn js_permissions_and_own() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-permission-chown".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) + .unwrap(); + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + Arc::new(OsApiMock::default()), + ) + .await + .unwrap() + .unwrap(); + } + #[tokio::test] + async fn js_action_test_zero_dir() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-zero-dir".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) + .unwrap(); + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + Arc::new(OsApiMock::default()), + ) + .await + .unwrap() + .unwrap(); + } + + #[tokio::test] + async fn js_rsync() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-rsync".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) + .unwrap(); + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + Arc::new(OsApiMock::default()), + ) + .await + .unwrap() + .unwrap(); + } + #[tokio::test] + async fn test_callback() { + let api = Arc::new(OsApiMock::default()); + let action_api = api.clone(); + let spawned = tokio::spawn(async move { + let mut watching = api.config_callbacks.subscribe(); + loop { + if watching.borrow().is_empty() { + watching.changed().await.unwrap(); + continue; + } + api.config_callbacks.send_modify(|x| { + x[0](json!("This is something across the wire!")) + .map_err(|e| format!("Failed call")) + .unwrap(); + }); + break; + } + }); + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-callback".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) + .unwrap(); + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + action_api, + ) + .await + .unwrap() + .unwrap(); + spawned.await.unwrap(); + } } #[tokio::test] diff --git a/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js b/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js index 769750ae4..c14017b1d 100644 --- a/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js +++ b/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js @@ -730,7 +730,7 @@ export async function setConfig(effects) { const assert = (condition, message) => { if (!condition) { - throw new Error(message); + throw ({error: message}); } }; const ackermann = (m, n) => { @@ -1038,6 +1038,93 @@ export const action = { .catch(() => {}); } }, + /** + * Testing callbacks? + * @param {*} effects + * @param {*} _input + * @returns + */ + async "test-callback"(effects, _input) { + await Promise.race([ + new Promise(done => effects.getServiceConfig({serviceId: 'something', configPath: "string", onChange: done})), + new Promise (async () => { + await effects.sleep(100) + throw new Error("Currently in sleeping") + } + )]) + + return { + result: { + copyable: false, + message: "Done", + version: "0", + qr: false, + }, + }; + }, + + /** + * We wanted to change the permissions and the ownership during the + * backing up, there where cases where the ownership is weird and + * broke for non root users. + * Note: Test for the chmod is broken and turned off because it only works when ran by root + * @param {*} effects + * @param {*} _input + * @returns + */ + async "test-permission-chown"(effects, _input) { + await effects + .removeDir({ + volumeId: "main", + path: "pem-chown", + }) + .catch(() => {}); + await effects.createDir({ + volumeId: "main", + path: "pem-chown/deep/123", + }); + await effects.writeFile({ + volumeId: "main", + path: "pem-chown/deep/123/test.txt", + toWrite: "Hello World", + }); + + const firstMetaData = await effects.metadata({ + volumeId: 'main', + path: 'pem-chown/deep/123/test.txt', + }) + assert(firstMetaData.readonly === false, `The readonly (${firstMetaData.readonly}) is wrong`); + const previousUid = firstMetaData.uid; + const expected = 1234 + + await effects.setPermissions({ + volumeId: 'main', + path: 'pem-chown/deep/123/test.txt', + readonly: true + }) + const chownError = await effects.chown({ + volumeId: 'main', + path: 'pem-chown/deep', + uid: expected + }).then(() => true, () => false) + let metaData = await effects.metadata({ + volumeId: 'main', + path: 'pem-chown/deep/123/test.txt', + }) + assert(metaData.readonly === true, `The readonly (${metaData.readonly}) is wrong`); + if (chownError) { + assert(metaData.uid === expected, `The uuid (${metaData.uid}) is wrong, should be more than ${previousUid}`); + } + + return { + result: { + copyable: false, + message: "Done", + version: "0", + qr: false, + }, + }; + }, async "test-disk-usage"(effects, _input) { const usage = await effects.diskUsage() @@ -1045,3 +1132,4 @@ export const action = { } }; + diff --git a/backend/test/js_action_execute/package-data/volumes/test-package/data/main/pem-chown/deep/123/test.txt b/backend/test/js_action_execute/package-data/volumes/test-package/data/main/pem-chown/deep/123/test.txt new file mode 100644 index 000000000..5e1c309da --- /dev/null +++ b/backend/test/js_action_execute/package-data/volumes/test-package/data/main/pem-chown/deep/123/test.txt @@ -0,0 +1 @@ +Hello World \ No newline at end of file diff --git a/libs/helpers/src/os_api.rs b/libs/helpers/src/os_api.rs index 1067195f3..07f7ba1ce 100644 --- a/libs/helpers/src/os_api.rs +++ b/libs/helpers/src/os_api.rs @@ -1,6 +1,7 @@ use color_eyre::eyre::eyre; -use models::Error; +use color_eyre::Report; use models::PackageId; +use models::{Error, InterfaceId}; use serde_json::Value; pub struct RuntimeDropped; @@ -13,6 +14,28 @@ fn method_not_available() -> Error { models::ErrorKind::InvalidRequest, ) } +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AddressSchemaOnion { + pub id: InterfaceId, + pub external_port: u16, +} +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AddressSchemaLocal { + pub id: InterfaceId, + pub external_port: u16, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Address(pub String); +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Domain; +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Name; #[async_trait::async_trait] #[allow(unused_variables)] @@ -22,7 +45,47 @@ pub trait OsApi: Send + Sync + 'static { id: PackageId, path: &str, callback: Callback, - ) -> Result { - Err(method_not_available()) + ) -> Result; + + async fn bind_local( + &self, + internal_port: u16, + address_schema: AddressSchemaLocal, + ) -> Result; + async fn bind_onion( + &self, + internal_port: u16, + address_schema: AddressSchemaOnion, + ) -> Result; + + async fn unbind_local(&self, id: InterfaceId, external: u16) -> Result<(), Report> { + todo!() + } + async fn unbind_onion(&self, id: InterfaceId, external: u16) -> Result<(), Report> { + todo!() + } + async fn list_address(&self) -> Result, Report> { + todo!() + } + async fn list_domains(&self) -> Result, Report> { + todo!() + } + async fn alloc_onion(&self, id: String) -> Result { + todo!() + } + async fn dealloc_onion(&self, id: String) -> Result<(), Report> { + todo!() + } + async fn alloc_local(&self, id: String) -> Result { + todo!() + } + async fn dealloc_local(&self, id: String) -> Result<(), Report> { + todo!() + } + async fn alloc_forward(&self, id: String) -> Result { + todo!() + } + async fn dealloc_forward(&self, id: String) -> Result<(), Report> { + todo!() } } diff --git a/libs/helpers/src/rsync.rs b/libs/helpers/src/rsync.rs index c09ac3d64..95f2df28d 100644 --- a/libs/helpers/src/rsync.rs +++ b/libs/helpers/src/rsync.rs @@ -70,6 +70,9 @@ impl Rsync { for exclude in options.exclude { cmd.arg(format!("--exclude={}", exclude)); } + if options.no_permissions { + cmd.arg("--no-perms"); + } let mut command = cmd .arg("-acAXH") .arg("--info=progress2") diff --git a/libs/js_engine/src/artifacts/loadModule.js b/libs/js_engine/src/artifacts/loadModule.js index ba39e837f..a285f7bf8 100644 --- a/libs/js_engine/src/artifacts/loadModule.js +++ b/libs/js_engine/src/artifacts/loadModule.js @@ -1,10 +1,24 @@ import Deno from "/deno_global.js"; import * as mainModule from "/embassy.js"; +// throw new Error("I'm going crasy") + function requireParam(param) { throw new Error(`Missing required parameter ${param}`); } +const callbackName = (() => { + let count = 0; + return () => `callback${count++}${Math.floor(Math.random() * 100000)}`; +})(); + +const callbackMapping = {}; +const registerCallback = (fn) => { + const uuid = callbackName(); // TODO + callbackMapping[uuid] = fn; + return uuid; +}; + /** * This is using the simplified json pointer spec, using no escapes and arrays * @param {object} obj @@ -35,42 +49,75 @@ const writeFile = ( ) => Deno.core.opAsync("write_file", volumeId, path, toWrite); const readFile = ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), + { + volumeId = requireParam("volumeId"), + path = requireParam("path"), + } = requireParam("options"), ) => Deno.core.opAsync("read_file", volumeId, path); - - const runDaemon = ( { command = requireParam("command"), args = [] } = requireParam("options"), ) => { let id = Deno.core.opAsync("start_command", command, args, "inherit", null); - let processId = id.then(x => x.processId) + let processId = id.then((x) => x.processId); let waitPromise = null; return { processId, async wait() { - waitPromise = waitPromise || Deno.core.opAsync("wait_command", await processId) - return waitPromise + waitPromise = waitPromise || + Deno.core.opAsync("wait_command", await processId); + return waitPromise; }, async term(signal = 15) { - return Deno.core.opAsync("send_signal", await processId, 15) - } - } + return Deno.core.opAsync("send_signal", await processId, 15); + }, + }; }; const runCommand = async ( - { command = requireParam("command"), args = [], timeoutMillis = 30000 } = requireParam("options"), + { + command = requireParam("command"), + args = [], + timeoutMillis = 30000, + } = requireParam("options"), ) => { - let id = Deno.core.opAsync("start_command", command, args, "collect", timeoutMillis); - let pid = id.then(x => x.processId) - return Deno.core.opAsync("wait_command", await pid) + let id = Deno.core.opAsync( + "start_command", + command, + args, + "collect", + timeoutMillis, + ); + let pid = id.then((x) => x.processId); + return Deno.core.opAsync("wait_command", await pid); }; +const bindLocal = async ( + { + internalPort = requireParam("internalPort"), + name = requireParam("name"), + externalPort = requireParam("externalPort"), + } = requireParam("options"), +) => { + return Deno.core.opAsync("bind_local", internalPort, { name, externalPort }); +}; +const bindTor = async ( + { + internalPort = requireParam("internalPort"), + name = requireParam("name"), + externalPort = requireParam("externalPort"), + } = requireParam("options"), +) => { + return Deno.core.opAsync("bind_onion", internalPort, { name, externalPort }); +}; + const signalGroup = async ( - { gid = requireParam("gid"), signal = requireParam("signal") } = requireParam("gid and signal") + { gid = requireParam("gid"), signal = requireParam("signal") } = requireParam( + "gid and signal", + ), ) => { return Deno.core.opAsync("signal_group", gid, signal); }; -const sleep = (timeMs = requireParam("timeMs"), -) => Deno.core.opAsync("sleep", timeMs); +const sleep = (timeMs = requireParam("timeMs")) => + Deno.core.opAsync("sleep", timeMs); const rename = ( { @@ -81,7 +128,10 @@ const rename = ( } = requireParam("options"), ) => Deno.core.opAsync("rename", srcVolume, srcPath, dstVolume, dstPath); const metadata = async ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), + { + volumeId = requireParam("volumeId"), + path = requireParam("path"), + } = requireParam("options"), ) => { const data = await Deno.core.opAsync("metadata", volumeId, path); return { @@ -92,7 +142,10 @@ const metadata = async ( }; }; const removeFile = ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), + { + volumeId = requireParam("volumeId"), + path = requireParam("path"), + } = requireParam("options"), ) => Deno.core.opAsync("remove_file", volumeId, path); const isSandboxed = () => Deno.core.opSync("is_sandboxed"); @@ -129,24 +182,38 @@ const chmod = async ( return await Deno.core.opAsync("chmod", volumeId, path, mode); }; const readJsonFile = async ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), + { + volumeId = requireParam("volumeId"), + path = requireParam("path"), + } = requireParam("options"), ) => JSON.parse(await readFile({ volumeId, path })); const createDir = ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), + { + volumeId = requireParam("volumeId"), + path = requireParam("path"), + } = requireParam("options"), ) => Deno.core.opAsync("create_dir", volumeId, path); const readDir = ( { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), ) => Deno.core.opAsync("read_dir", volumeId, path); const removeDir = ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), + { + volumeId = requireParam("volumeId"), + path = requireParam("path"), + } = requireParam("options"), ) => Deno.core.opAsync("remove_dir", volumeId, path); -const trace = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_trace", whatToTrace); -const warn = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_warn", whatToTrace); -const error = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_error", whatToTrace); -const debug = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_debug", whatToTrace); -const info = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_info", whatToTrace); -const fetch = async (url = requireParam ('url'), options = null) => { +const trace = (whatToTrace = requireParam("whatToTrace")) => + Deno.core.opAsync("log_trace", whatToTrace); +const warn = (whatToTrace = requireParam("whatToTrace")) => + Deno.core.opAsync("log_warn", whatToTrace); +const error = (whatToTrace = requireParam("whatToTrace")) => + Deno.core.opAsync("log_error", whatToTrace); +const debug = (whatToTrace = requireParam("whatToTrace")) => + Deno.core.opAsync("log_debug", whatToTrace); +const info = (whatToTrace = requireParam("whatToTrace")) => + Deno.core.opAsync("log_info", whatToTrace); +const fetch = async (url = requireParam("url"), options = null) => { const { body, ...response } = await Deno.core.opAsync("fetch", url, options); const textValue = Promise.resolve(body); return { @@ -161,28 +228,35 @@ const fetch = async (url = requireParam ('url'), options = null) => { }; const runRsync = ( - { + { srcVolume = requireParam("srcVolume"), dstVolume = requireParam("dstVolume"), srcPath = requireParam("srcPath"), dstPath = requireParam("dstPath"), options = requireParam("options"), - } = requireParam("options"), + } = requireParam("options"), ) => { - let id = Deno.core.opAsync("rsync", srcVolume, srcPath, dstVolume, dstPath, options); + let id = Deno.core.opAsync( + "rsync", + srcVolume, + srcPath, + dstVolume, + dstPath, + options, + ); let waitPromise = null; return { async id() { - return id + return id; }, async wait() { - waitPromise = waitPromise || Deno.core.opAsync("rsync_wait", await id) - return waitPromise + waitPromise = waitPromise || Deno.core.opAsync("rsync_wait", await id); + return waitPromise; }, async progress() { - return Deno.core.opAsync("rsync_progress", await id) - } - } + return Deno.core.opAsync("rsync_progress", await id); + }, + }; }; const diskUsage = async ({ @@ -193,63 +267,105 @@ const diskUsage = async ({ return { used, total } } -const callbackMapping = {} -const registerCallback = (fn) => { - const uuid = generateUuid(); // TODO - callbackMapping[uuid] = fn; - return uuid -} -const runCallback = (uuid, data) => callbackMapping[uuid](data) +globalThis.runCallback = (uuid, data) => callbackMapping[uuid](data); +// window.runCallback = runCallback; +// Deno.runCallback = runCallback; -const getServiceConfig = async (serviceId, configPath, onChange) => { - await Deno.core.opAsync("get_service_config", serviceId, configPath, registerCallback(onChange)) -} +const getServiceConfig = async ( + { + serviceId = requireParam("serviceId"), + configPath = requireParam("configPath"), + onChange = requireParam("onChange"), + } = requireParam("options"), +) => { + return await Deno.core.opAsync( + "get_service_config", + serviceId, + configPath, + registerCallback(onChange), + ); +}; + +const setPermissions = async ( + { + volumeId = requireParam("volumeId"), + path = requireParam("path"), + readonly = requireParam("readonly"), + } = requireParam("options"), +) => { + return await Deno.core.opAsync("set_permissions", volumeId, path, readonly); +}; const currentFunction = Deno.core.opSync("current_function"); const input = Deno.core.opSync("get_input"); const variable_args = Deno.core.opSync("get_variable_args"); -const setState = (x) => Deno.core.opSync("set_value", x); +const setState = (x) => Deno.core.opAsync("set_value", x); const effects = { + bindLocal, + bindTor, chmod, chown, - writeFile, - readFile, - writeJsonFile, - readJsonFile, - error, - warn, + createDir, debug, - trace, + diskUsage, + error, + fetch, + getServiceConfig, + getServiceConfig, info, isSandboxed, - fetch, - removeFile, - createDir, - removeDir, metadata, + readDir, + readFile, + readJsonFile, + removeDir, + removeFile, rename, runCommand, - sleep, runDaemon, - signalGroup, runRsync, - readDir, - diskUsage, - getServiceConfig, + setPermissions, + signalGroup, + sleep, + trace, + warn, + writeFile, + writeJsonFile, }; const defaults = { - "handleSignal": (effects, { gid, signal }) => { - return effects.signalGroup({ gid, signal }) + handleSignal: (effects, { gid, signal }) => { + return effects.signalGroup({ gid, signal }); + }, +}; + +function safeToString(fn, orValue = "") { + try { + return fn(); + } catch (e) { + return orValue; } } -const runFunction = jsonPointerValue(mainModule, currentFunction) || jsonPointerValue(defaults, currentFunction); +const runFunction = jsonPointerValue(mainModule, currentFunction) || + jsonPointerValue(defaults, currentFunction); (async () => { - if (typeof runFunction !== "function") { - error(`Expecting ${currentFunction} to be a function`); - throw new Error(`Expecting ${currentFunction} to be a function`); - } - const answer = await runFunction(effects, input, ...variable_args); - setState(answer); + const answer = await (async () => { + if (typeof runFunction !== "function") { + error(`Expecting ${currentFunction} to be a function`); + throw new Error(`Expecting ${currentFunction} to be a function`); + } + })() + .then(() => runFunction(effects, input, ...variable_args)) + .catch((e) => { + if ("error" in e) return e; + if ("error-code" in e) return e; + return { + error: safeToString( + () => e.toString(), + "Error Not able to be stringified", + ), + }; + }); + await setState(answer); })(); diff --git a/libs/js_engine/src/lib.rs b/libs/js_engine/src/lib.rs index c2d196c39..41d407c34 100644 --- a/libs/js_engine/src/lib.rs +++ b/libs/js_engine/src/lib.rs @@ -109,8 +109,15 @@ enum ResultType { ErrorCode(i32, String), Result(serde_json::Value), } -#[derive(Clone, Default)] -struct AnswerState(std::sync::Arc>); +#[derive(Clone)] +struct AnswerState(mpsc::Sender); + +impl AnswerState { + fn new() -> (Self, mpsc::Receiver) { + let (send, recv) = mpsc::channel(1); + (Self(send), recv) + } +} #[derive(Clone, Debug)] struct ModsLoader { @@ -282,6 +289,9 @@ impl JsExecutionEnvironment { vec![ fns::chown::decl(), fns::chmod::decl(), + fns::bind_local::decl(), + fns::bind_onion::decl(), + fns::chown::decl(), fns::fetch::decl(), fns::read_file::decl(), fns::metadata::decl(), @@ -306,6 +316,7 @@ impl JsExecutionEnvironment { fns::wait_command::decl(), fns::sleep::decl(), fns::send_signal::decl(), + fns::set_permissions::decl(), fns::signal_group::decl(), fns::rsync::decl(), fns::rsync_wait::decl(), @@ -321,7 +332,7 @@ impl JsExecutionEnvironment { variable_args: Vec, ) -> Result { let base_directory = self.base_directory.clone(); - let answer_state = AnswerState::default(); + let (answer_state, mut receive_answer) = AnswerState::new(); let ext_answer_state = answer_state.clone(); let (callback_sender, callback_receiver) = mpsc::unbounded_channel(); let js_ctx = JsContext { @@ -376,12 +387,18 @@ impl JsExecutionEnvironment { Ok::<_, AnyError>(()) }; - future.await.map_err(|e| { - tracing::debug!("{:?}", e); - (JsError::Javascript, format!("{}", e)) - })?; + let answer = tokio::select! { + Some(x) = receive_answer.recv() => x, + _ = future => { + if let Some(x) = receive_answer.recv().await { + x + } + else { + serde_json::json!({"error": "JS Engine Shutdown"}) + } + }, - let answer = answer_state.0.lock().clone(); + }; Ok(answer) } } @@ -399,10 +416,10 @@ impl<'a> Future for RuntimeEventLoop<'a> { ) -> std::task::Poll { let this = self.project(); if let Poll::Ready(Some((uuid, value))) = this.callback.poll_recv(cx) { - match this - .runtime - .execute_script("callback", &format!("runCallback({uuid}, {value})")) - { + match this.runtime.execute_script( + "callback", + &format!("globalThis.runCallback(\"{uuid}\", {value})"), + ) { Ok(_) => (), Err(e) => return Poll::Ready(Err(e)), } @@ -440,7 +457,10 @@ mod fns { OutputParams, OutputStrategy, ProcessGroupId, ProcessId, RunCommand, RunCommandParams, SendSignal, SendSignalParams, SignalGroup, SignalGroupParams, }; - use helpers::{to_tmp_path, AtomicFile, Rsync, RsyncOptions, RuntimeDropped}; + use helpers::{ + to_tmp_path, AddressSchemaLocal, AddressSchemaOnion, AtomicFile, Rsync, RsyncOptions, + RuntimeDropped, + }; use itertools::Itertools; use models::{PackageId, VolumeId}; use serde::{Deserialize, Serialize}; @@ -751,7 +771,7 @@ mod fns { volume_path.to_string_lossy(), ); } - if let Err(_) = tokio::fs::metadata(&src).await { + if tokio::fs::metadata(&src).await.is_err() { bail!("Source at {} does not exists", src.to_string_lossy()); } @@ -813,6 +833,39 @@ mod fns { Ok(progress) } #[op] + async fn set_permissions( + state: Rc>, + volume_id: VolumeId, + path_in: PathBuf, + readonly: bool, + ) -> Result<(), AnyError> { + let (volumes, volume_path) = { + let state = state.borrow(); + let ctx: &JsContext = state.borrow(); + let volume_path = ctx + .volumes + .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) + .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))?; + (ctx.volumes.clone(), volume_path) + }; + if volumes.readonly(&volume_id) { + bail!("Volume {} is readonly", volume_id); + } + let new_file = volume_path.join(path_in); + // With the volume check + if !is_subset(&volume_path, &new_file).await? { + bail!( + "Path '{}' has broken away from parent '{}'", + new_file.to_string_lossy(), + volume_path.to_string_lossy(), + ); + } + let mut perms = tokio::fs::metadata(&new_file).await?.permissions(); + perms.set_readonly(readonly); + tokio::fs::set_permissions(new_file, perms).await?; + Ok(()) + } + #[op] async fn remove_file( state: Rc>, volume_id: VolumeId, @@ -1039,8 +1092,10 @@ mod fns { #[op] async fn log_trace(state: Rc>, input: String) -> Result<(), AnyError> { - let state = state.borrow(); - let ctx = state.borrow::().clone(); + let ctx = { + let state = state.borrow(); + state.borrow::().clone() + }; if let Some(rpc_client) = ctx.container_rpc_client { return rpc_client .request( @@ -1063,8 +1118,10 @@ mod fns { } #[op] async fn log_warn(state: Rc>, input: String) -> Result<(), AnyError> { - let state = state.borrow(); - let ctx = state.borrow::().clone(); + let ctx = { + let state = state.borrow(); + state.borrow::().clone() + }; if let Some(rpc_client) = ctx.container_rpc_client { return rpc_client .request( @@ -1087,8 +1144,10 @@ mod fns { } #[op] async fn log_error(state: Rc>, input: String) -> Result<(), AnyError> { - let state = state.borrow(); - let ctx = state.borrow::().clone(); + let ctx = { + let state = state.borrow(); + state.borrow::().clone() + }; if let Some(rpc_client) = ctx.container_rpc_client { return rpc_client .request( @@ -1111,8 +1170,10 @@ mod fns { } #[op] async fn log_debug(state: Rc>, input: String) -> Result<(), AnyError> { - let state = state.borrow(); - let ctx = state.borrow::().clone(); + let ctx = { + let state = state.borrow(); + state.borrow::().clone() + }; if let Some(rpc_client) = ctx.container_rpc_client { return rpc_client .request( @@ -1135,8 +1196,10 @@ mod fns { } #[op] async fn log_info(state: Rc>, input: String) -> Result<(), AnyError> { - let state = state.borrow(); - let ctx = state.borrow::().clone(); + let ctx = { + let state = state.borrow(); + state.borrow::().clone() + }; if let Some(rpc_client) = ctx.container_rpc_client { return rpc_client .request( @@ -1169,9 +1232,16 @@ mod fns { Ok(ctx.variable_args.clone()) } #[op] - fn set_value(state: &mut OpState, value: Value) -> Result<(), AnyError> { - let mut answer = state.borrow::().0.lock(); - *answer = value; + async fn set_value(state: Rc>, value: Value) -> Result<(), AnyError> { + let sender = { + let state = state.borrow(); + let answer_state = state.borrow::().0.clone(); + answer_state + }; + sender + .send(value) + .await + .map_err(|_e| anyhow!("Could not set a value"))?; Ok(()) } #[op] @@ -1424,28 +1494,54 @@ mod fns { service_id: PackageId, path: String, callback: String, - ) -> Result { - let state = state.borrow(); - let ctx = state.borrow::(); - let sender = ctx.callback_sender.clone(); - Ok( - match ctx - .os - .get_service_config( - service_id, - &path, - Box::new(move |value| { - sender - .send((callback.clone(), value)) - .map_err(|_| RuntimeDropped) - }), - ) - .await - { - Ok(a) => ResultType::Result(a), - Err(e) => ResultType::ErrorCode(e.kind as i32, e.source.to_string()), - }, + ) -> Result { + let (sender, os) = { + let state = state.borrow(); + let ctx = state.borrow::(); + (ctx.callback_sender.clone(), ctx.os.clone()) + }; + os.get_service_config( + service_id, + &path, + Box::new(move |value| { + sender + .send((callback.clone(), value)) + .map_err(|_| RuntimeDropped) + }), ) + .await + .map_err(|e| anyhow!("Couldn't get service config: {e:?}")) + } + + #[op] + async fn bind_onion( + state: Rc>, + internal_port: u16, + address_schema: AddressSchemaOnion, + ) -> Result { + let os = { + let state = state.borrow(); + let ctx = state.borrow::(); + ctx.os.clone() + }; + os.bind_onion(internal_port, address_schema) + .await + .map_err(|e| anyhow!("{e:?}")) + } + #[op] + async fn bind_local( + state: Rc>, + internal_port: u16, + address_schema: AddressSchemaLocal, + ) -> Result { + let os = { + let state = state.borrow(); + let ctx = state.borrow::(); + ctx.os.clone() + }; + os.bind_local(internal_port, address_schema) + .await + .map_err(|e| anyhow!("{e:?}")) } /// We need to make sure that during the file accessing, we don't reach beyond our scope of control