From b5927e47119d52fff3f210fc6ba96c99d257f5f7 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sun, 28 Jul 2019 09:57:26 -0300 Subject: [PATCH] chore(merge) feature/event-system (#12) * feat(event-system) prototype * feat(event-system) prepare two way communication * feat(event-system) fASLR * feat(event-system) answer salt * feat(event-system) simplify communication and enable multi-level two way message passing --- lib/rust/Cargo.toml | 4 +- lib/rust/src/api.rs | 177 ++++++++++++++++++++++-------------- lib/rust/src/api/cmd.rs | 19 +++- lib/rust/src/command.rs | 4 +- lib/rust/src/dir.rs | 1 - lib/rust/src/event.rs | 78 ++++++++++++++++ lib/rust/src/file_system.rs | 12 +-- lib/rust/src/lib.rs | 17 +++- lib/rust/src/salt.rs | 63 +++++++++++++ templates/rust/src/main.rs | 49 ++++++++-- 10 files changed, 332 insertions(+), 92 deletions(-) create mode 100644 lib/rust/src/event.rs create mode 100644 lib/rust/src/salt.rs diff --git a/lib/rust/Cargo.toml b/lib/rust/Cargo.toml index 8f94edd4c..8e2826766 100644 --- a/lib/rust/Cargo.toml +++ b/lib/rust/Cargo.toml @@ -27,9 +27,10 @@ flate2 = "1" hyper-old-types = "0.11.0" sysinfo = "0.9" webbrowser = "0.5.1" +uuid = { version = "0.7", features = ["v4"] } +lazy_static = "1.3.0" [features] -api = [] all-api = [] readTextFile = [] readBinaryFile = [] @@ -39,3 +40,4 @@ listDirs = [] setTitle = [] execute = [] open = [] +answer = [] diff --git a/lib/rust/src/api.rs b/lib/rust/src/api.rs index 2abe10bbe..e652d3e99 100644 --- a/lib/rust/src/api.rs +++ b/lib/rust/src/api.rs @@ -4,78 +4,115 @@ use proton_ui::WebView; #[allow(unused_variables)] pub fn handler(webview: &mut WebView<'_, T>, arg: &str) -> bool { - #[cfg(feature = "api")] - { - use cmd::Cmd::*; - match serde_json::from_str(arg) { - Err(_) => false, - Ok(command) => { - match command { - #[cfg(any(feature = "all-api", feature = "readTextFile"))] - ReadTextFile { - path, - callback, - error, - } => { - super::file_system::read_text_file(webview, path, callback, error); - } - #[cfg(any(feature = "all-api", feature = "readBinaryFile"))] - ReadBinaryFile { - path, - callback, - error, - } => { - super::file_system::read_binary_file(webview, path, callback, error); - } - #[cfg(any(feature = "all-api", feature = "writeFile"))] - WriteFile { - file, - contents, - callback, - error, - } => { - super::file_system::write_file(webview, file, contents, callback, error); - } - #[cfg(any(feature = "all-api", feature = "listDirs"))] - ListDirs { - path, - callback, - error, - } => { - super::file_system::list_dirs(webview, path, callback, error); - } - #[cfg(any(feature = "all-api", feature = "listFiles"))] - ListFiles { - path, - callback, - error, - } => { - super::file_system::list(webview, path, callback, error); - } - #[cfg(any(feature = "all-api", feature = "setTitle"))] - SetTitle { title } => { - webview.set_title(&title).unwrap(); - } - #[cfg(any(feature = "all-api", feature = "execute"))] - Execute { - command, - args, - callback, - error, - } => { - super::command::call(webview, command, args, callback, error); - }, - #[cfg(any(feature = "all-api", feature = "open"))] - Open { uri } => { - super::spawn(move || { - webbrowser::open(&uri).unwrap(); - }); - } + use cmd::Cmd::*; + match serde_json::from_str(arg) { + Err(_) => false, + Ok(command) => { + match command { + #[cfg(any(feature = "all-api", feature = "readTextFile"))] + ReadTextFile { + path, + callback, + error, + } => { + super::file_system::read_text_file(webview, path, callback, error); + } + #[cfg(any(feature = "all-api", feature = "readBinaryFile"))] + ReadBinaryFile { + path, + callback, + error, + } => { + super::file_system::read_binary_file(webview, path, callback, error); + } + #[cfg(any(feature = "all-api", feature = "writeFile"))] + WriteFile { + file, + contents, + callback, + error, + } => { + super::file_system::write_file(webview, file, contents, callback, error); + } + #[cfg(any(feature = "all-api", feature = "listDirs"))] + ListDirs { + path, + callback, + error, + } => { + super::file_system::list_dirs(webview, path, callback, error); + } + #[cfg(any(feature = "all-api", feature = "listFiles"))] + ListFiles { + path, + callback, + error, + } => { + super::file_system::list(webview, path, callback, error); + } + #[cfg(any(feature = "all-api", feature = "setTitle"))] + SetTitle { title } => { + webview.set_title(&title).unwrap(); + } + #[cfg(any(feature = "all-api", feature = "execute"))] + Execute { + command, + args, + callback, + error, + } => { + super::command::call(webview, command, args, callback, error); + } + #[cfg(any(feature = "all-api", feature = "open"))] + Open { uri } => { + super::spawn(move || { + webbrowser::open(&uri).unwrap(); + }); + } + + ValidateSalt { + salt, + callback, + error, + } => { + crate::salt::validate(webview, salt, callback, error); + } + AddEventListener { + event, + handler, + once, + } => { + webview + .eval(&format!( + " + if (window['{obj}'] === void 0) {{ + window['{obj}'] = {{}} + }} + if (window['{obj}']['{evt}'] === void 0) {{ + window['{obj}']['{evt}'] = [] + }} + window['{obj}']['{evt}'].push({{ + handler: window['{handler}'], + once: {once_flag} + }}) + ", + obj = crate::event::event_listeners_object_name(), + evt = event, + handler = handler, + once_flag = if once { "true" } else { "false" } + )) + .unwrap(); + } + #[cfg(any(feature = "all-api", feature = "answer"))] + Answer { + event_id, + payload, + salt, + } => { + crate::event::answer(event_id, payload, salt); } - true } + true } } - #[cfg(not(feature = "api"))] - false } diff --git a/lib/rust/src/api/cmd.rs b/lib/rust/src/api/cmd.rs index 519ca643d..badebf07c 100644 --- a/lib/rust/src/api/cmd.rs +++ b/lib/rust/src/api/cmd.rs @@ -1,6 +1,5 @@ #[derive(Deserialize)] #[serde(tag = "cmd", rename_all = "camelCase")] -#[cfg(feature = "api")] pub enum Cmd { #[cfg(any(feature = "all-api", feature = "readTextFile"))] ReadTextFile { @@ -43,5 +42,21 @@ pub enum Cmd { error: String, }, #[cfg(any(feature = "all-api", feature = "open"))] - Open { uri: String } + Open { uri: String }, + ValidateSalt { + salt: String, + callback: String, + error: String, + }, + AddEventListener { + event: String, + handler: String, + once: bool, + }, + #[cfg(any(feature = "all-api", feature = "answer"))] + Answer { + event_id: String, + payload: String, + salt: String, + }, } diff --git a/lib/rust/src/command.rs b/lib/rust/src/command.rs index 76f3b6079..a1ea6ad4f 100755 --- a/lib/rust/src/command.rs +++ b/lib/rust/src/command.rs @@ -2,7 +2,7 @@ use proton_ui::WebView; use std::process::{Child, Command, Stdio}; -use super::run_async; +use crate::execute_promise; pub fn get_output(cmd: String, args: Vec, stdout: Stdio) -> Result { Command::new(cmd) @@ -65,7 +65,7 @@ pub fn call( callback: String, error: String, ) { - run_async( + execute_promise( webview, || { get_output(command, args, Stdio::piped()) diff --git a/lib/rust/src/dir.rs b/lib/rust/src/dir.rs index ee1401bf9..392ae6bd0 100644 --- a/lib/rust/src/dir.rs +++ b/lib/rust/src/dir.rs @@ -1,4 +1,3 @@ - use tempfile; mod utils; diff --git a/lib/rust/src/event.rs b/lib/rust/src/event.rs new file mode 100644 index 000000000..ea82edc4d --- /dev/null +++ b/lib/rust/src/event.rs @@ -0,0 +1,78 @@ +use proton_ui::{Handle, WebView}; +use std::boxed::Box; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +struct EventHandler { + on_event: Box, +} + +thread_local!(static LISTENERS: Arc>> = Arc::new(Mutex::new(HashMap::new()))); + +lazy_static! { + static ref PROMPT_FUNCTION_NAME: String = uuid::Uuid::new_v4().to_string(); + static ref EVENT_LISTENERS_OBJECT_NAME: String = uuid::Uuid::new_v4().to_string(); +} + +pub fn prompt_function_name() -> String { + PROMPT_FUNCTION_NAME.to_string() +} + +pub fn event_listeners_object_name() -> String { + EVENT_LISTENERS_OBJECT_NAME.to_string() +} + +pub fn prompt( + webview: &mut WebView<'_, T>, + id: &'static str, + payload: String, + handler: F, +) { + LISTENERS.with(|listeners| { + let mut l = listeners.lock().unwrap(); + l.insert( + id.to_string(), + EventHandler { + on_event: Box::new(handler), + }, + ); + }); + + trigger(webview.handle(), id, payload); +} + +pub fn trigger(webview_handle: Handle, id: &'static str, mut payload: String) { + let salt = crate::salt::generate(); + if payload == "" { + payload = "void 0".to_string(); + } + + webview_handle + .dispatch(move |_webview| { + _webview.eval(&format!( + "window['{}']({{type: '{}', payload: {}}}, '{}')", + prompt_function_name(), + id, + payload, + salt + )) + }) + .unwrap(); +} + +pub fn answer(id: String, data: String, salt: String) { + if crate::salt::is_valid(salt) { + LISTENERS.with(|l| { + let mut listeners = l.lock().unwrap(); + + let key = id.clone(); + + if listeners.contains_key(&id) { + let handler = listeners.remove(&id).unwrap(); + (handler.on_event)(data); + } + + listeners.remove(&key); + }); + } +} diff --git a/lib/rust/src/file_system.rs b/lib/rust/src/file_system.rs index 2dd7cabdc..c75485235 100755 --- a/lib/rust/src/file_system.rs +++ b/lib/rust/src/file_system.rs @@ -1,8 +1,8 @@ use proton_ui::WebView; use crate::dir; +use crate::execute_promise; use crate::file; -use crate::run_async; use std::fs::File; use std::io::Write; @@ -13,7 +13,7 @@ pub fn list( callback: String, error: String, ) { - run_async( + execute_promise( webview, move || { dir::walk_dir(path.to_string()) @@ -30,7 +30,7 @@ pub fn list_dirs( callback: String, error: String, ) { - run_async( + execute_promise( webview, move || { dir::list_dir_contents(&path) @@ -48,7 +48,7 @@ pub fn write_file( callback: String, error: String, ) { - run_async( + execute_promise( webview, move || { File::create(file) @@ -70,7 +70,7 @@ pub fn read_text_file( callback: String, error: String, ) { - run_async( + execute_promise( webview, move || { file::read_string(path).and_then(|f| { @@ -90,7 +90,7 @@ pub fn read_binary_file( callback: String, error: String, ) { - run_async( + execute_promise( webview, move || { file::read_binary(path).and_then(|f| { diff --git a/lib/rust/src/lib.rs b/lib/rust/src/lib.rs index 7aa56685d..b4f28bbef 100644 --- a/lib/rust/src/lib.rs +++ b/lib/rust/src/lib.rs @@ -4,15 +4,20 @@ extern crate serde_derive; #[macro_use] mod macros; +#[macro_use] +extern crate lazy_static; + pub mod api; pub mod command; pub mod dir; +pub mod event; pub mod file; pub mod file_system; pub mod http; pub mod platform; pub mod process; pub mod rpc; +pub mod salt; pub mod tcp; pub mod updater; pub mod version; @@ -23,9 +28,7 @@ use threadpool::ThreadPool; thread_local!(static POOL: ThreadPool = ThreadPool::new(4)); -pub fn spawn () + Send + 'static>( - what: F -) { +pub fn spawn () + Send + 'static>(what: F) { POOL.with(|thread| { thread.execute(move || { what(); @@ -33,7 +36,13 @@ pub fn spawn () + Send + 'static>( }); } -pub fn run_async Result + Send + 'static>( +pub fn run_async () + Send + 'static>(what: F) { + POOL.with(|thread| { + thread.execute(move || what()); + }); +} + +pub fn execute_promise Result + Send + 'static>( webview: &mut WebView<'_, T>, what: F, callback: String, diff --git a/lib/rust/src/salt.rs b/lib/rust/src/salt.rs new file mode 100644 index 000000000..b4a73eb39 --- /dev/null +++ b/lib/rust/src/salt.rs @@ -0,0 +1,63 @@ +use proton_ui::WebView; +use std::sync::Mutex; +use uuid::Uuid; + +struct Salt { + value: String, + one_time: bool, +} + +lazy_static! { + static ref SALTS: Mutex> = Mutex::new(vec![]); +} + +pub fn generate() -> String { + let salt = Uuid::new_v4(); + SALTS.lock().unwrap().push(Salt { + value: salt.to_string(), + one_time: true, + }); + return salt.to_string(); +} + +pub fn generate_static() -> String { + let salt = Uuid::new_v4(); + SALTS.lock().unwrap().push(Salt { + value: salt.to_string(), + one_time: false, + }); + return salt.to_string(); +} + +pub fn is_valid(salt: String) -> bool { + let mut salts = SALTS.lock().unwrap(); + match salts.iter().position(|s| s.value == salt) { + Some(index) => { + if salts[index].one_time { + salts.remove(index); + } + true + } + None => false, + } +} + +pub fn validate( + webview: &mut WebView<'_, T>, + salt: String, + callback: String, + error: String, +) { + crate::execute_promise( + webview, + move || { + if is_valid(salt) { + Ok("'VALID'".to_string()) + } else { + Err("'INVALID SALT'".to_string()) + } + }, + callback, + error, + ); +} diff --git a/templates/rust/src/main.rs b/templates/rust/src/main.rs index 8248d4d84..fbaf250df 100755 --- a/templates/rust/src/main.rs +++ b/templates/rust/src/main.rs @@ -69,14 +69,14 @@ fn main() { debug = cfg!(debug_assertions); #[cfg(feature = "serverless")] { - fn inline_style(s: &str) -> String { + fn inline_style(s: &str) -> String { format!(r#""#, s) - } + } - fn inline_script(s: &str) -> String { + fn inline_script(s: &str) -> String { format!(r#""#, s) - } - let html = format!(r#"{styles}
{scripts}"#, + } + let html = format!(r#"{styles}
{scripts}"#, styles = inline_style(include_str!("../target/compiled-web/css/app.css")), scripts = inline_script(include_str!("../target/compiled-web/js/app.js")), ); @@ -123,9 +123,46 @@ fn main() { .build() .unwrap(); + webview + .handle() + .dispatch(move |_webview| { + _webview + .eval(&format!( + "window['{fn}'] = (payload, salt) => {{ + window.proton.promisified({{ + cmd: 'validateSalt', + salt + }}).then(() => {{ + const listeners = (window['{obj}'] && window['{obj}'][payload.type]) || [] + for (let i = listeners.length - 1; i >= 0; i--) {{ + const listener = listeners[i] + if (listener.once) + listeners.splice(i, 1) + const response = listener.handler(payload) + response && response + .then(result => {{ + window.proton.invoke({{ + cmd: 'answer', + event_id: payload.type, + payload: result, + salt: '{salt}' + }}) + }}) + }} + }}) + }}", + fn = proton::event::prompt_function_name(), + obj = proton::event::event_listeners_object_name(), + salt = proton::salt::generate_static() + )) + .unwrap(); + + Ok(()) + }) + .unwrap(); + #[cfg(not(feature = "dev"))] { - #[cfg(not(feature = "serverless"))] { thread::spawn(move || {