Bladeren bron

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
Lucas Fernandes Nogueira 6 jaren geleden
bovenliggende
commit
b5927e4711

+ 3 - 1
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 = []

+ 107 - 70
lib/rust/src/api.rs

@@ -4,78 +4,115 @@ use proton_ui::WebView;
 
 #[allow(unused_variables)]
 pub fn handler<T: 'static>(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
 }

+ 17 - 2
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,
+  },
 }

+ 2 - 2
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<String>, stdout: Stdio) -> Result<String, String> {
   Command::new(cmd)
@@ -65,7 +65,7 @@ pub fn call<T: 'static>(
   callback: String,
   error: String,
 ) {
-  run_async(
+  execute_promise(
     webview,
     || {
       get_output(command, args, Stdio::piped())

+ 0 - 1
lib/rust/src/dir.rs

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

+ 78 - 0
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<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);
+    });
+  }
+}

+ 6 - 6
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<T: 'static>(
   callback: String,
   error: String,
 ) {
-  run_async(
+  execute_promise(
     webview,
     move || {
       dir::walk_dir(path.to_string())
@@ -30,7 +30,7 @@ pub fn list_dirs<T: 'static>(
   callback: String,
   error: String,
 ) {
-  run_async(
+  execute_promise(
     webview,
     move || {
       dir::list_dir_contents(&path)
@@ -48,7 +48,7 @@ pub fn write_file<T: 'static>(
   callback: String,
   error: String,
 ) {
-  run_async(
+  execute_promise(
     webview,
     move || {
       File::create(file)
@@ -70,7 +70,7 @@ pub fn read_text_file<T: 'static>(
   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<T: 'static>(
   callback: String,
   error: String,
 ) {
-  run_async(
+  execute_promise(
     webview,
     move || {
       file::read_binary(path).and_then(|f| {

+ 13 - 4
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<F: FnOnce() -> () + Send + 'static>(
-  what: F
-) {
+pub fn spawn<F: FnOnce() -> () + Send + 'static>(what: F) {
   POOL.with(|thread| {
     thread.execute(move || {
       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>,
   what: F,
   callback: String,

+ 63 - 0
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<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,
+  );
+}

+ 43 - 6
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#"<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)
-    }
-    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")),
     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 || {