diff --git a/Cargo.lock b/Cargo.lock index a913814e4..959acbb39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,7 +365,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 0.38.42", "slab", "tracing", "windows-sys 0.59.0", @@ -774,7 +774,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "tiny-keccak", ] @@ -1337,10 +1337,22 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1886,6 +1898,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" + [[package]] name = "litemap" version = "0.7.4" @@ -1957,7 +1975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2303,7 +2321,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 0.38.42", "tracing", "windows-sys 0.59.0", ] @@ -2403,7 +2421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", - "getrandom", + "getrandom 0.2.15", "rand", "ring", "rustc-hash", @@ -2472,7 +2490,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -2626,7 +2644,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -2758,7 +2776,20 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.14", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.2", "windows-sys 0.59.0", ] @@ -3239,14 +3270,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.14.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", "once_cell", - "rustix", + "rustix 1.0.2", "windows-sys 0.59.0", ] @@ -3721,7 +3753,7 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ - "getrandom", + "getrandom 0.2.15", "serde", ] @@ -3745,9 +3777,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -3767,6 +3799,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.93" @@ -3879,6 +3920,16 @@ dependencies = [ "url", ] +[[package]] +name = "web-prover-executor" +version = "0.1.0" +dependencies = [ + "tempfile", + "tracing", + "uuid", + "wait-timeout", +] + [[package]] name = "web-prover-notary" version = "0.7.0" @@ -4112,6 +4163,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index f0ab4aabe..29cac4711 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members =["client", "notary", "core", "tests"] +members =["client", "notary", "core", "tests", "executor"] resolver="2" [workspace.dependencies] @@ -38,6 +38,9 @@ uuid ={ version="1.10.0", default-features=false, features=["v4", "serde"] tracing-test="0.2" +tempfile ="3.18.0" +wait-timeout="0.2.1" + [profile.dev] incremental =true opt-level =1 diff --git a/README.md b/README.md index f1d6a0034..129cb84c1 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ If you have any questions, please reach out to any of Pluto's [team members](htt ### Usage ``` -cargo run -p notary -- --config ./fixture/notary-config.toml -cargo run -p client -- --config ./fixture/client.proxy.json +cargo run -p web-prover-notary -- --config ./fixture/notary-config.toml +cargo run -p web-prover-client -- --config ./fixture/client.proxy.json ``` ## Security Status diff --git a/executor/Cargo.toml b/executor/Cargo.toml new file mode 100644 index 000000000..536379fc1 --- /dev/null +++ b/executor/Cargo.toml @@ -0,0 +1,10 @@ +[package] +edition="2021" +name ="web-prover-executor" +version="0.1.0" + +[dependencies] +tempfile ={ workspace=true } +tracing ={ workspace=true } +uuid ={ workspace=true } +wait-timeout={ workspace=true } diff --git a/executor/README.md b/executor/README.md new file mode 100644 index 000000000..27f22adb0 --- /dev/null +++ b/executor/README.md @@ -0,0 +1,26 @@ +# Web Prover Executor + +## Set up playground + +``` +npx playwright install +npm install -g playwright-core + +git clone git@github.com:pluto/playwright-playground.git +cd playwright-playground +npm install -g ./playwright-utils + +export NODE_PATH=$(npm root -g) +``` + +## Run example + +Run notary in a separate terminal: +``` +RUST_LOG=debug cargo run -p web-prover-notary -- --config ./fixture/notary-config.toml +``` + +Run example executor: +``` +cargo run -p web-prover-executor +``` \ No newline at end of file diff --git a/executor/src/lib.rs b/executor/src/lib.rs new file mode 100644 index 000000000..6d6e1590d --- /dev/null +++ b/executor/src/lib.rs @@ -0,0 +1 @@ +mod playwright; diff --git a/executor/src/playwright.rs b/executor/src/playwright.rs new file mode 100644 index 000000000..78cb2ae0c --- /dev/null +++ b/executor/src/playwright.rs @@ -0,0 +1,206 @@ +use std::{ + error::Error, + io::{Read, Write}, + path::PathBuf, + process::{Command, Stdio}, + time::Duration, +}; + +use tempfile::NamedTempFile; +use tracing::{debug, error}; +use uuid::Uuid; +use wait_timeout::ChildExt; + +/// The Playwright template with a placeholder for the script +const PLAYWRIGHT_TEMPLATE: &str = r#" +const { chromium } = require('playwright-core'); +const { prompt, prove, setSessionUUID } = require("@plutoxyz/playwright-utils"); + +(async () => { + const sessionUUID = process.argv[2]; + setSessionUUID(sessionUUID); + console.log("Starting Playwright session with UUID:", sessionUUID); + + const browser = await chromium.launch({ + headless: true, + executablePath: '/Users/darkrai/Library/Caches/ms-playwright/chromium_headless_shell-1155/chrome-mac/headless_shell' + }); + const context = await browser.newContext(); + const page = await context.newPage(); + + // Developer provided script: + {{.Script}} + + await browser.close(); +})(); +"#; + +/// Configuration for the Playwright runner +pub struct PlaywrightRunnerConfig { + /// Developer script to run in the Playwright template + script: String, + /// Timeout for script execution in seconds + pub timeout_seconds: u64, +} + +pub struct PlaywrightRunner { + /// scipt template with placeholder for the developer script + template: String, + /// Playwright runner configuration + config: PlaywrightRunnerConfig, + /// Path to the Node.js executable + node_path: PathBuf, + /// environment variables + env: Vec<(String, String)>, +} + +#[derive(Debug)] +pub struct PlaywrightOutput { + pub stdout: String, + pub stderr: String, +} + +// TODO: add a PlaywrightError type + +impl PlaywrightRunner { + pub fn new( + config: PlaywrightRunnerConfig, + template: String, + node_path: PathBuf, + env_vars: Vec<(String, String)>, + ) -> Self { + Self { config, template, node_path, env: env_vars } + } + + pub fn run_script(&self, session_id: Uuid) -> Result> { + // fill the template with the developer script + let template = self.template.replace("{{.Script}}", &self.config.script); + + // create a temporary file to store the template + let mut temp_file = NamedTempFile::new()?; + temp_file.write_all(template.as_bytes())?; + let temp_path = temp_file.path().to_owned(); + let temp_dir = temp_path.parent().unwrap(); + + // close the file to flush the buffer + let _temp_file = temp_file.into_temp_path(); + + // Execute the command with timeout + debug!("Starting Playwright session id: {}", session_id); + let mut command = Command::new(&self.node_path); + let command = command + .arg(&temp_path) + .arg(session_id.to_string()) + .current_dir(temp_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + // Add environment variables + for (key, value) in &self.env { + command.env(key, value); + } + + let mut child = command.spawn()?; + + // Set a timeout + let timeout = Duration::from_secs(self.config.timeout_seconds); + let _ = match child.wait_timeout(timeout)? { + Some(status) => + if let Some(code) = status.code() { + code + } else { + error!("Process terminated by signal: {:?}", status); + return Err("Process terminated by signal".into()); + }, + None => { + child.kill()?; + error!("Process timed out after {:?}", timeout); + return Err("Process timed out".into()); + }, + }; + + // Convert output to string + let stdout = match child.stdout.take() { + Some(mut stdout_stream) => { + let mut stdout = String::new(); + stdout_stream.read_to_string(&mut stdout)?; + stdout + }, + None => String::new(), + }; + + let stderr = match child.stderr.take() { + Some(mut stderr_stream) => { + let mut stderr = String::new(); + stderr_stream.read_to_string(&mut stderr)?; + stderr + }, + None => String::new(), + }; + + let output = PlaywrightOutput { stdout, stderr }; + + Ok(output) + } +} + +mod tests { + + use super::*; + + const EXAMPLE_DEVELOPER_SCRIPT: &str = r#" +await page.goto("https://pseudo-bank.pluto.dev"); + +const username = page.getByRole("textbox", { name: "Username" }); +const password = page.getByRole("textbox", { name: "Password" }); + +let input = await prompt([ + { title: "Username", types: "text" }, + { title: "Password", types: "password" }, +]); + +await username.fill(input.inputs[0]); +await password.fill(input.inputs[1]); + +const loginBtn = page.getByRole("button", { name: "Login" }); +await loginBtn.click(); + +await page.waitForSelector("text=Your Accounts", { timeout: 5000 }); + +const balanceLocator = page.locator("\#balance-2"); +await balanceLocator.waitFor({ state: "visible", timeout: 5000 }); +const balanceText = (await balanceLocator.textContent()) || ""; +const balance = parseFloat(balanceText.replace(/[$,]/g, "")); + +await prove("bank_balance", balance); +"#; + + #[test] + fn test_playwright_script() { + // Example developer script to inject into the Playwright template + let session_id = Uuid::new_v4(); + // output of `which node` + let node_path = + Command::new("which").arg("node").output().expect("Failed to run `which node`").stdout; + let node_path = String::from_utf8_lossy(&node_path).trim().to_string(); + + let config = PlaywrightRunnerConfig { + script: EXAMPLE_DEVELOPER_SCRIPT.to_string(), + timeout_seconds: 30, + }; + let runner = PlaywrightRunner::new( + config, + PLAYWRIGHT_TEMPLATE.to_string(), + PathBuf::from(node_path), + vec![(String::from("DEBUG"), String::from("pw:api"))], + ); + + let result = runner.run_script(session_id); + + if let Err(e) = result { + eprintln!("Failed to run Playwright script: {:?}", e); + } else { + println!("output: {:?}", result.unwrap()); + } + } +} diff --git a/notary/src/config.rs b/notary/src/config.rs index b399ff171..33f2505b1 100644 --- a/notary/src/config.rs +++ b/notary/src/config.rs @@ -15,6 +15,7 @@ pub struct Config { pub server_cert: String, pub server_key: String, pub listen: String, + pub listen_internal: String, pub notary_signing_key: String, pub acme_email: String, pub acme_domain: String, @@ -27,6 +28,7 @@ pub fn read_config() -> Config { let builder = config::Config::builder() // TODO is this the right way to make server_cert optional? .set_default("listen", "0.0.0.0:443").unwrap() + .set_default("listen_internal", "127.0.0.1:7935").unwrap() .set_default("server_cert", "").unwrap() .set_default("server_key", "").unwrap() .set_default("notary_signing_key", "").unwrap() diff --git a/notary/src/main.rs b/notary/src/main.rs index 1491f4a83..dbdc3e840 100644 --- a/notary/src/main.rs +++ b/notary/src/main.rs @@ -31,6 +31,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod config; mod error; mod proxy; +mod runner; mod verifier; struct SharedState { @@ -94,6 +95,18 @@ async fn main() -> Result<(), NotaryServerError> { .layer(CorsLayer::permissive()) .with_state(shared_state); + // Create a separate internal router for prompts + // and Start the internal HTTP server as a separate task + let internal_router = + Router::new().route("/prompt", post(runner::prompt)).route("/prove", post(runner::prove)); + let internal_listener = TcpListener::bind(&c.listen_internal).await?; + info!("Internal server listening on http://{}", &c.listen_internal); + tokio::spawn(async move { + if let Err(e) = axum::serve(internal_listener, internal_router).await { + error!("Internal server error: {:?}", e); + } + }); + if !c.server_cert.is_empty() || !c.server_key.is_empty() { let _ = listen(listener, router, &c.server_cert, &c.server_key).await; } else { diff --git a/notary/src/runner.rs b/notary/src/runner.rs new file mode 100644 index 000000000..e8b6f37a9 --- /dev/null +++ b/notary/src/runner.rs @@ -0,0 +1,50 @@ +use axum::{ + extract::{self}, + Json, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::debug; + +use crate::error::NotaryServerError; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Prompt { + pub title: String, + pub types: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PromptRequest { + pub uuid: String, + pub prompts: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct PromptResponse { + pub inputs: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ProveRequest { + pub uuid: String, + pub key: String, + pub value: Value, +} + +pub async fn prompt( + extract::Json(payload): extract::Json, +) -> Result, NotaryServerError> { + debug!("Prompting: {:?}", payload); + let inputs = payload.prompts.iter().map(|prompt| prompt.title.clone()).collect(); + let response = PromptResponse { inputs }; + + Ok(Json(response)) +} + +pub async fn prove( + extract::Json(payload): extract::Json, +) -> Result, NotaryServerError> { + debug!("Proving: {:?}", payload); + Ok(Json(())) +}