diff --git a/Cargo.toml b/Cargo.toml index a063b6c..3330918 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,9 @@ uuid = { version = "0.8.2", features = ["serde", "v4"] } mio = { version = "0.7", features = ["os-poll", "net"] } derive_more = "0.99" # for DNS from hermes lazy_static = "1.4.0" +#tiny_http = "0.8" +tungstenite = "0.13" +actix-web = "3" # Optional dependencies regulated by features web-view = { version = "0.7", features = [], optional = true } diff --git a/src/lib.rs b/src/lib.rs index a035bdb..9af108b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,4 +23,5 @@ pub mod dns_utils; pub mod settings; pub mod bytes; pub mod crypto; +pub mod web_server; diff --git a/src/main.rs b/src/main.rs index 79007e6..e03d0bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ use std::io::{Seek, SeekFrom}; use std::sync::atomic::{AtomicBool, Ordering}; use alfis::keystore::create_key; use alfis::eventbus::register; +use alfis::web_server::start_server; #[cfg(feature = "webgui")] mod web_ui; @@ -174,6 +175,11 @@ fn main() { let mut network = Network::new(Arc::clone(&context)); network.start().expect("Error starting network component"); + // Starting internal Web-server to provide HTML/CSS/JS for UI + thread::spawn(|| { + let _ = start_server(); + }); + create_genesis_if_needed(&context, &miner); if no_gui { print_my_domains(&context); diff --git a/src/web_server.rs b/src/web_server.rs new file mode 100644 index 0000000..810ac23 --- /dev/null +++ b/src/web_server.rs @@ -0,0 +1,53 @@ +use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; + +pub const CONTENT_TYPE_HTML: &str = "text/html; charset=utf-8"; +pub const CONTENT_TYPE_JS: &str = "application/javascript"; +pub const CONTENT_TYPE_CSS: &str = "text/css"; + +#[get("/")] +async fn html() -> impl Responder { + let body = include_str!("webview/index.html"); + HttpResponse::Ok().body(body) +} + +#[get("/bulma.css")] +async fn bulma() -> impl Responder { + let body = include_str!("webview/bulma.css"); + HttpResponse::Ok().content_type(CONTENT_TYPE_CSS).body(body) +} + +#[get("/styles.css")] +async fn styles() -> impl Responder { + let body = include_str!("webview/styles.css"); + HttpResponse::Ok().content_type(CONTENT_TYPE_CSS).body(body) +} + +#[get("/busy.css")] +async fn busy() -> impl Responder { + let body = include_str!("webview/busy.css"); + HttpResponse::Ok().content_type(CONTENT_TYPE_CSS).body(body) +} + +#[get("/scripts.js")] +async fn scripts() -> impl Responder { + let body = include_str!("webview/scripts.js"); + HttpResponse::Ok().content_type(CONTENT_TYPE_JS).body(body) +} + +#[post("/api")] +async fn api(req_body: String) -> impl Responder { + HttpResponse::Ok().body("Not implemented") +} + +#[actix_web::main] +pub async fn start_server() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .service(html) + .service(bulma) + .service(styles) + .service(busy) + .service(scripts) + .service(api) + }).bind("127.0.0.1:4280")?.run().await +} \ No newline at end of file diff --git a/src/web_ui.rs b/src/web_ui.rs index 7739b40..4ff51c8 100644 --- a/src/web_ui.rs +++ b/src/web_ui.rs @@ -12,7 +12,6 @@ use chrono::{DateTime, Local}; #[allow(unused_imports)] use log::{debug, error, info, LevelFilter, trace, warn}; use serde::Deserialize; -use web_view::Content; use alfis::{Block, Bytes, Context, Keystore, Transaction}; use alfis::keystore; @@ -27,44 +26,20 @@ use Cmd::*; use self::web_view::{Handle, WebView}; use alfis::crypto::CryptoBox; use alfis::eventbus::{register, post}; +use self::web_view::Content::Url; pub fn run_interface(context: Arc>, miner: Arc>) { - let file_content = include_str!("webview/index.html"); - let mut styles = inline_style(include_str!("webview/bulma.css")); - styles.push_str(&inline_style(include_str!("webview/styles.css"))); - styles.push_str(&inline_style(include_str!("webview/busy_indicator.css"))); - let scripts = inline_script(include_str!("webview/scripts.js")); - - let html = Content::Html(file_content.to_owned().replace("{styles}", &styles).replace("{scripts}", &scripts)); let title = format!("ALFIS {}", env!("CARGO_PKG_VERSION")); let mut interface = web_view::builder() .title(&title) - .content(html) + .content(Url("http://localhost:4280/")) .size(1023, 720) .min_size(773, 350) .resizable(true) .debug(false) .user_data(()) .invoke_handler(|web_view, arg| { - debug!("Command {}", arg); - match serde_json::from_str(arg).unwrap() { - Loaded => { action_loaded(&context, web_view); } - LoadKey => { action_load_key(&context, web_view); } - CreateKey => { keystore::create_key(Arc::clone(&context)); } - SaveKey => { action_save_key(&context); } - CheckRecord { data } => { action_check_record(web_view, data); } - CheckDomain { name } => { action_check_domain(&context, web_view, name); } - MineDomain { name, data, signing, encryption } => { - action_create_domain(Arc::clone(&context), Arc::clone(&miner), web_view, name, data, signing, encryption); - } - TransferDomain { .. } => {} - StopMining => { post(Event::ActionStopMining); } - Open { link } => { - if open::that(&link).is_err() { - show_warning(web_view, "Something wrong, I can't open the link 😢"); - } - } - } + handle_command(web_view, &context, &miner, arg); Ok(()) }) .build() @@ -73,6 +48,28 @@ pub fn run_interface(context: Arc>, miner: Arc>) { run_interface_loop(&mut interface); } +fn handle_command(web_view: &mut WebView<()>, context: &Arc>, miner: &Arc>, arg: &str) { + debug!("Command {}", arg); + match serde_json::from_str(arg).unwrap() { + Loaded => { action_loaded(&context, web_view); } + LoadKey => { action_load_key(&context, web_view); } + CreateKey => { keystore::create_key(Arc::clone(&context)); } + SaveKey => { action_save_key(&context); } + CheckRecord { data } => { action_check_record(web_view, data); } + CheckDomain { name } => { action_check_domain(&context, web_view, name); } + MineDomain { name, data, signing, encryption } => { + action_create_domain(Arc::clone(&context), Arc::clone(&miner), web_view, name, data, signing, encryption); + } + TransferDomain { .. } => {} + StopMining => { post(Event::ActionStopMining); } + Open { link } => { + if open::that(&link).is_err() { + show_warning(web_view, "Something wrong, I can't open the link 😢"); + } + } + } +} + /// Indefinitely loops through WebView steps fn run_interface_loop(interface: &mut WebView<()>) { // We use this ugly loop to lower CPU usage a lot. @@ -570,11 +567,3 @@ impl Status { self.speed.iter().sum() } } - -fn inline_style(s: &str) -> String { - format!(r#""#, s) -} - -fn inline_script(s: &str) -> String { - format!(r#""#, s) -} diff --git a/src/webview/busy_indicator.css b/src/webview/busy.css similarity index 100% rename from src/webview/busy_indicator.css rename to src/webview/busy.css diff --git a/src/webview/index.html b/src/webview/index.html index 010abc1..0b9d899 100644 --- a/src/webview/index.html +++ b/src/webview/index.html @@ -4,8 +4,10 @@ ALFIS - {scripts} - {styles} + + + + diff --git a/src/webview/scripts.js b/src/webview/scripts.js index fa0fe0d..915a22c 100644 --- a/src/webview/scripts.js +++ b/src/webview/scripts.js @@ -1,3 +1,5 @@ +API_URL = "http://localhost:4280/api"; + var recordsBuffer = []; var ownerSigning = ""; var ownerEncryption = ""; @@ -9,6 +11,38 @@ document.addEventListener('click', function (event) { closeDropdowns(); }); +API = {} +API.invokeJson = function(json) { + external.invoke(json); +} +API.invokeCmd = function(cmd) { + external.invoke(JSON.stringify(cmd)); +} +API.postCmd = function(cmd) { + var xhr = new XMLHttpRequest(); + xhr.open("POST", API_URL, true); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.send(JSON.stringify(cmd)); +} + +// Defines handler for external API +function defineExternalApi() { + // Workaround for Arch Linux Webkit + // https://github.com/Boscop/web-view/issues/212#issuecomment-671055663 + if (typeof window.external == 'undefined' || typeof window.external.invoke == 'undefined') { + window.external = { + invoke: function(x) { + window.webkit.messageHandlers.external.postMessage(x); + } + }; + } +} + +function onLoad() { + defineExternalApi(); + API.invokeCmd({cmd: 'loaded'}); +} + function closeDropdowns(except) { // Get all elements with class="dropdowns" and hide them var dropdowns = document.getElementsByClassName("dropdown is-active"); @@ -180,20 +214,6 @@ function editDomain(domain, event) { }); } -function onLoad() { - // Workaround for Arch Linux Webkit - // https://github.com/Boscop/web-view/issues/212#issuecomment-671055663 - if (typeof window.external == 'undefined' || typeof window.external.invoke == 'undefined') { - window.external = { - invoke: function(x) { - window.webkit.messageHandlers.external.postMessage(x); - } - }; - } - - external.invoke(JSON.stringify({cmd: 'loaded'})); -} - function openTab(element, tabName) { // Declare all variables var i, tabContent, tabLinks; @@ -223,23 +243,23 @@ function toggle(element, event) { } function open_link(link) { - external.invoke(JSON.stringify({cmd: 'open', link: link})); + API.invokeCmd({cmd: 'open', link: link}); } function loadKey() { - external.invoke(JSON.stringify({cmd: 'loadKey'})); + API.invokeCmd({cmd: 'loadKey'}); } function createKey() { - external.invoke(JSON.stringify({cmd: 'createKey'})); + API.invokeCmd({cmd: 'createKey'}); } function saveKey() { - external.invoke(JSON.stringify({cmd: 'saveKey'})); + API.invokeCmd({cmd: 'saveKey'}); } function checkRecord(data) { - external.invoke(JSON.stringify({cmd: 'checkRecord', data: JSON.stringify(data)})); + API.invokeCmd({cmd: 'checkRecord', data: JSON.stringify(data)}); } function recordOkay(okay) { @@ -274,7 +294,7 @@ function createDomain() { data.records = recordsBuffer; data.contacts = getContacts(); data = JSON.stringify(data); - external.invoke(JSON.stringify({cmd: 'mineDomain', name: domain, data: data, signing: ownerSigning, encryption: ownerEncryption})); + API.invokeCmd({cmd: 'mineDomain', name: domain, data: data, signing: ownerSigning, encryption: ownerEncryption}); } function getContacts() { @@ -315,14 +335,10 @@ function domainMiningUnavailable() { document.getElementById("new_key_button").disabled = false; } -function sendAction(param) { - external.invoke(JSON.stringify(param)); -} - function onDomainChange(element) { if (typeof currentZone !== 'undefined') { var domain = element.value + "." + currentZone.name; - external.invoke(JSON.stringify({cmd: 'checkDomain', name: domain})); + API.invokeCmd({cmd: 'checkDomain', name: domain}); } } @@ -480,7 +496,7 @@ function showMiningIndicator(visible, blue) { function miningIndicatorClick(element) { showModalDialog("Do you really want to stop mining?", function() { - external.invoke(JSON.stringify({cmd: 'stopMining'})); + API.invokeCmd({cmd: 'stopMining'}); }); }