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
This commit is contained in:
Lucas Fernandes Nogueira 2019-07-28 09:57:26 -03:00 committed by GitHub
parent fcd0c7fc35
commit b5927e4711
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 332 additions and 92 deletions

View File

@ -27,9 +27,10 @@ flate2 = "1"
hyper-old-types = "0.11.0" hyper-old-types = "0.11.0"
sysinfo = "0.9" sysinfo = "0.9"
webbrowser = "0.5.1" webbrowser = "0.5.1"
uuid = { version = "0.7", features = ["v4"] }
lazy_static = "1.3.0"
[features] [features]
api = []
all-api = [] all-api = []
readTextFile = [] readTextFile = []
readBinaryFile = [] readBinaryFile = []
@ -39,3 +40,4 @@ listDirs = []
setTitle = [] setTitle = []
execute = [] execute = []
open = [] open = []
answer = []

View File

@ -4,78 +4,115 @@ use proton_ui::WebView;
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn handler<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> bool { pub fn handler<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> bool {
#[cfg(feature = "api")] use cmd::Cmd::*;
{ match serde_json::from_str(arg) {
use cmd::Cmd::*; Err(_) => false,
match serde_json::from_str(arg) { Ok(command) => {
Err(_) => false, match command {
Ok(command) => { #[cfg(any(feature = "all-api", feature = "readTextFile"))]
match command { ReadTextFile {
#[cfg(any(feature = "all-api", feature = "readTextFile"))] path,
ReadTextFile { callback,
path, error,
callback, } => {
error, super::file_system::read_text_file(webview, path, callback, error);
} => { }
super::file_system::read_text_file(webview, path, callback, error); #[cfg(any(feature = "all-api", feature = "readBinaryFile"))]
} ReadBinaryFile {
#[cfg(any(feature = "all-api", feature = "readBinaryFile"))] path,
ReadBinaryFile { callback,
path, error,
callback, } => {
error, super::file_system::read_binary_file(webview, path, callback, error);
} => { }
super::file_system::read_binary_file(webview, path, callback, error); #[cfg(any(feature = "all-api", feature = "writeFile"))]
} WriteFile {
#[cfg(any(feature = "all-api", feature = "writeFile"))] file,
WriteFile { contents,
file, callback,
contents, error,
callback, } => {
error, super::file_system::write_file(webview, file, contents, callback, error);
} => { }
super::file_system::write_file(webview, file, contents, callback, error); #[cfg(any(feature = "all-api", feature = "listDirs"))]
} ListDirs {
#[cfg(any(feature = "all-api", feature = "listDirs"))] path,
ListDirs { callback,
path, error,
callback, } => {
error, super::file_system::list_dirs(webview, path, callback, error);
} => { }
super::file_system::list_dirs(webview, path, callback, error); #[cfg(any(feature = "all-api", feature = "listFiles"))]
} ListFiles {
#[cfg(any(feature = "all-api", feature = "listFiles"))] path,
ListFiles { callback,
path, error,
callback, } => {
error, super::file_system::list(webview, path, callback, error);
} => { }
super::file_system::list(webview, path, callback, error); #[cfg(any(feature = "all-api", feature = "setTitle"))]
} SetTitle { title } => {
#[cfg(any(feature = "all-api", feature = "setTitle"))] webview.set_title(&title).unwrap();
SetTitle { title } => { }
webview.set_title(&title).unwrap(); #[cfg(any(feature = "all-api", feature = "execute"))]
} Execute {
#[cfg(any(feature = "all-api", feature = "execute"))] command,
Execute { args,
command, callback,
args, error,
callback, } => {
error, super::command::call(webview, command, args, callback, error);
} => { }
super::command::call(webview, command, args, callback, error); #[cfg(any(feature = "all-api", feature = "open"))]
}, Open { uri } => {
#[cfg(any(feature = "all-api", feature = "open"))] super::spawn(move || {
Open { uri } => { webbrowser::open(&uri).unwrap();
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
} }

View File

@ -1,6 +1,5 @@
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(tag = "cmd", rename_all = "camelCase")] #[serde(tag = "cmd", rename_all = "camelCase")]
#[cfg(feature = "api")]
pub enum Cmd { pub enum Cmd {
#[cfg(any(feature = "all-api", feature = "readTextFile"))] #[cfg(any(feature = "all-api", feature = "readTextFile"))]
ReadTextFile { ReadTextFile {
@ -43,5 +42,21 @@ pub enum Cmd {
error: String, error: String,
}, },
#[cfg(any(feature = "all-api", feature = "open"))] #[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,
},
} }

View File

@ -2,7 +2,7 @@ use proton_ui::WebView;
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};
use super::run_async; use crate::execute_promise;
pub fn get_output(cmd: String, args: Vec<String>, stdout: Stdio) -> Result<String, String> { pub fn get_output(cmd: String, args: Vec<String>, stdout: Stdio) -> Result<String, String> {
Command::new(cmd) Command::new(cmd)
@ -65,7 +65,7 @@ pub fn call<T: 'static>(
callback: String, callback: String,
error: String, error: String,
) { ) {
run_async( execute_promise(
webview, webview,
|| { || {
get_output(command, args, Stdio::piped()) get_output(command, args, Stdio::piped())

View File

@ -1,4 +1,3 @@
use tempfile; use tempfile;
mod utils; mod utils;

78
lib/rust/src/event.rs Normal file
View File

@ -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<dyn FnOnce(String)>,
}
thread_local!(static LISTENERS: Arc<Mutex<HashMap<String, EventHandler>>> = 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<T: 'static, F: FnOnce(String) + 'static>(
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<T: 'static>(webview_handle: Handle<T>, 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);
});
}
}

View File

@ -1,8 +1,8 @@
use proton_ui::WebView; use proton_ui::WebView;
use crate::dir; use crate::dir;
use crate::execute_promise;
use crate::file; use crate::file;
use crate::run_async;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
@ -13,7 +13,7 @@ pub fn list<T: 'static>(
callback: String, callback: String,
error: String, error: String,
) { ) {
run_async( execute_promise(
webview, webview,
move || { move || {
dir::walk_dir(path.to_string()) dir::walk_dir(path.to_string())
@ -30,7 +30,7 @@ pub fn list_dirs<T: 'static>(
callback: String, callback: String,
error: String, error: String,
) { ) {
run_async( execute_promise(
webview, webview,
move || { move || {
dir::list_dir_contents(&path) dir::list_dir_contents(&path)
@ -48,7 +48,7 @@ pub fn write_file<T: 'static>(
callback: String, callback: String,
error: String, error: String,
) { ) {
run_async( execute_promise(
webview, webview,
move || { move || {
File::create(file) File::create(file)
@ -70,7 +70,7 @@ pub fn read_text_file<T: 'static>(
callback: String, callback: String,
error: String, error: String,
) { ) {
run_async( execute_promise(
webview, webview,
move || { move || {
file::read_string(path).and_then(|f| { file::read_string(path).and_then(|f| {
@ -90,7 +90,7 @@ pub fn read_binary_file<T: 'static>(
callback: String, callback: String,
error: String, error: String,
) { ) {
run_async( execute_promise(
webview, webview,
move || { move || {
file::read_binary(path).and_then(|f| { file::read_binary(path).and_then(|f| {

View File

@ -4,15 +4,20 @@ extern crate serde_derive;
#[macro_use] #[macro_use]
mod macros; mod macros;
#[macro_use]
extern crate lazy_static;
pub mod api; pub mod api;
pub mod command; pub mod command;
pub mod dir; pub mod dir;
pub mod event;
pub mod file; pub mod file;
pub mod file_system; pub mod file_system;
pub mod http; pub mod http;
pub mod platform; pub mod platform;
pub mod process; pub mod process;
pub mod rpc; pub mod rpc;
pub mod salt;
pub mod tcp; pub mod tcp;
pub mod updater; pub mod updater;
pub mod version; pub mod version;
@ -23,9 +28,7 @@ use threadpool::ThreadPool;
thread_local!(static POOL: ThreadPool = ThreadPool::new(4)); thread_local!(static POOL: ThreadPool = ThreadPool::new(4));
pub fn spawn<F: FnOnce() -> () + Send + 'static>( pub fn spawn<F: FnOnce() -> () + Send + 'static>(what: F) {
what: F
) {
POOL.with(|thread| { POOL.with(|thread| {
thread.execute(move || { thread.execute(move || {
what(); what();
@ -33,7 +36,13 @@ pub fn spawn<F: FnOnce() -> () + Send + 'static>(
}); });
} }
pub fn run_async<T: 'static, F: FnOnce() -> Result<String, String> + Send + 'static>( pub fn run_async<F: FnOnce() -> () + Send + 'static>(what: F) {
POOL.with(|thread| {
thread.execute(move || what());
});
}
pub fn execute_promise<T: 'static, F: FnOnce() -> Result<String, String> + Send + 'static>(
webview: &mut WebView<'_, T>, webview: &mut WebView<'_, T>,
what: F, what: F,
callback: String, callback: String,

63
lib/rust/src/salt.rs Normal file
View File

@ -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<Vec<Salt>> = 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<T: 'static>(
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,
);
}

View File

@ -69,14 +69,14 @@ fn main() {
debug = cfg!(debug_assertions); debug = cfg!(debug_assertions);
#[cfg(feature = "serverless")] #[cfg(feature = "serverless")]
{ {
fn inline_style(s: &str) -> String { fn inline_style(s: &str) -> String {
format!(r#"<style type="text/css">{}</style>"#, s) format!(r#"<style type="text/css">{}</style>"#, s)
} }
fn inline_script(s: &str) -> String { fn inline_script(s: &str) -> String {
format!(r#"<script type="text/javascript">{}</script>"#, s) format!(r#"<script type="text/javascript">{}</script>"#, s)
} }
let html = format!(r#"<!DOCTYPE html><html><head><meta http-equiv="Content-Security-Policy" content="default-src data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'">{styles}</head><body><div id="q-app"></div>{scripts}</body></html>"#, let html = format!(r#"<!DOCTYPE html><html><head><meta http-equiv="Content-Security-Policy" content="default-src data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'">{styles}</head><body><div id="q-app"></div>{scripts}</body></html>"#,
styles = inline_style(include_str!("../target/compiled-web/css/app.css")), styles = inline_style(include_str!("../target/compiled-web/css/app.css")),
scripts = inline_script(include_str!("../target/compiled-web/js/app.js")), scripts = inline_script(include_str!("../target/compiled-web/js/app.js")),
); );
@ -123,9 +123,46 @@ fn main() {
.build() .build()
.unwrap(); .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 = "dev"))]
{ {
#[cfg(not(feature = "serverless"))] #[cfg(not(feature = "serverless"))]
{ {
thread::spawn(move || { thread::spawn(move || {