diff --git a/Cargo.lock b/Cargo.lock index 6041073..32a83e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,6 +149,7 @@ dependencies = [ "tokio", "u2fframing", "warp", + "webbrowser", ] [[package]] @@ -166,6 +167,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "bstr" version = "0.2.12" @@ -205,6 +215,12 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "0.1.10" @@ -284,6 +300,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes 1.4.0", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -615,6 +651,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.0" @@ -807,6 +852,28 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if 1.0.0", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.69" @@ -906,6 +973,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "num-traits" version = "0.2.11" @@ -925,6 +998,40 @@ dependencies = [ "libc", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "object" version = "0.36.0" @@ -1740,6 +1847,34 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "425ba64c1e13b1c6e8c5d2541c8fac10022ca584f33da781db01b5756aef1f4e" +dependencies = [ + "block2", + "core-foundation", + "home", + "jni", + "log", + "ndk-context", + "objc2", + "objc2-foundation", + "url", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1780,6 +1915,15 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1798,6 +1942,21 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.0" @@ -1829,6 +1988,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" @@ -1841,6 +2006,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" @@ -1853,6 +2024,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.0" @@ -1871,6 +2048,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.0" @@ -1883,6 +2066,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" @@ -1895,6 +2084,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" @@ -1907,6 +2102,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" diff --git a/bitbox-bridge/Cargo.toml b/bitbox-bridge/Cargo.toml index aa48a58..d9cb1f1 100644 --- a/bitbox-bridge/Cargo.toml +++ b/bitbox-bridge/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" license = "Apache-2.0" [dependencies] +webbrowser = "1.0" env_logger = "0.11" futures = { workspace = true } futures-util = { workspace = true } diff --git a/bitbox-bridge/resources/confirmation_dialog.html b/bitbox-bridge/resources/confirmation_dialog.html new file mode 100644 index 0000000..e1f6dc0 --- /dev/null +++ b/bitbox-bridge/resources/confirmation_dialog.html @@ -0,0 +1,20 @@ + + + + + Confirmation + + +

Confirm Action

+

{{ message }}

+ + + + + diff --git a/bitbox-bridge/src/main.rs b/bitbox-bridge/src/main.rs index 9e26cb8..858b3ad 100644 --- a/bitbox-bridge/src/main.rs +++ b/bitbox-bridge/src/main.rs @@ -13,7 +13,9 @@ // limitations under the License. use futures::channel::mpsc; +use std::collections::HashSet; use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; use tokio::runtime::Runtime; #[macro_use] @@ -90,9 +92,26 @@ fn main() -> Result<(), Box> { let addr = SocketAddr::new("127.0.0.1".parse()?, port); println!("listening on http://{}", addr); - let server = web::create(usb_devices, notify_tx, addr); + let state = web::ConfirmState::new(); + let remembered_allowed_origins = Arc::new(Mutex::new(HashSet::new())); + let server = web::create( + usb_devices, + notify_tx, + state.clone(), + remembered_allowed_origins.clone(), + addr, + ); rt.block_on(async move { + let s = state.clone(); + let r = remembered_allowed_origins.clone(); + tokio::spawn(async move { + let foo = web::user_confirm_origin(s, r, "ORIGIN".into(), &format!("http://{}", addr)) + .await + .unwrap(); + println!("RESULT 1 {}", foo); + }); + tokio::select! { _ = stop_request => info!("Requested to stop by environment"), _ = server => info!("Warp returned"), diff --git a/bitbox-bridge/src/web.rs b/bitbox-bridge/src/web.rs index b9890c6..fe0147c 100644 --- a/bitbox-bridge/src/web.rs +++ b/bitbox-bridge/src/web.rs @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use futures::channel::mpsc; +use futures::channel::{mpsc, oneshot}; use futures::prelude::*; use futures_util::sink::SinkExt; use percent_encoding::percent_decode_str; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; -use warp::{self, Filter, Rejection}; +use std::sync::atomic::AtomicU32; +use std::sync::{Arc, Mutex}; +use warp::{self, Filter, Rejection, Reply}; use crate::error::WebError; use crate::usb::UsbDevices; @@ -155,7 +157,137 @@ async fn ws_upgrade( })) } -pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: SocketAddr) { +// Global state to store the current oneshot sender +pub struct ConfirmState { + counter: AtomicU32, + sender: Mutex, String)>>, +} + +impl ConfirmState { + pub fn new() -> Arc { + Arc::new(ConfirmState { + counter: AtomicU32::new(0), + sender: Mutex::new(HashMap::new()), + }) + } +} + +fn with_state( + state: T, +) -> impl Filter + Clone { + warp::any().map(move || state.clone()) +} + +pub async fn user_confirm( + state: Arc, + message: String, + base_url: &str, +) -> Result { + let (tx, rx) = oneshot::channel(); + let counter = state + .counter + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + { + let mut sender = state.sender.lock().unwrap(); + sender.insert(counter, (tx, message)); + } + + // Launch the web browser to show the dialog + let dialog_url = format!("{}/confirm/{}", base_url, counter); + if webbrowser::open(&dialog_url).is_err() { + return Err(()); + } + + // Wait for the user's response from the HTTP handler + rx.await.map_err(|_| ()) +} + +pub async fn user_confirm_origin( + state: Arc, + remembered_allowed_origins: Arc>>, + origin: &str, + base_url: &str, +) -> Result { + { + // Early return if the origin was previously allowed/accepted by the user. + if remembered_allowed_origins.lock().unwrap().contains(origin) { + return Ok(true); + } + } + let result = user_confirm( + state, + format!("Allow {} to connect to your BitBox?", origin), + base_url, + ) + .await?; + if result { + remembered_allowed_origins + .lock() + .unwrap() + .insert(origin.into()); + } + Ok(result) +} + +fn setup_confirm_routes( + state: Arc, +) -> impl Filter + Clone { + let confirm_dialog = warp::path!("confirm" / u32) + .and(warp::get()) + .and(with_state(state.clone())) + .map(|counter: u32, state: Arc| { + let sender_locked = state.sender.lock().unwrap(); + if let Some((_, message)) = sender_locked.get(&counter) { + let html = include_str!("../resources/confirmation_dialog.html"); + let ctx = { + let mut ctx = tera::Context::new(); + ctx.insert("counter", &counter); + ctx.insert("message", &message); + ctx + }; + let body = match tera::Tera::one_off(html, &ctx, true) { + Ok(reply) => reply, + Err(_) => "Could not render tera template".into(), + }; + warp::reply::html(body).into_response() + } else { + // No user confirmation active. + warp::reply::with_status("", warp::http::StatusCode::BAD_REQUEST).into_response() + } + }); + + async fn handle_user_response( + counter: u32, + choice: bool, + state: Arc, + ) -> Result { + if let Some((sender, _)) = state.sender.lock().unwrap().remove(&counter) { + let _ = sender.send(choice); + + Ok(warp::reply::with_status("", warp::http::StatusCode::OK)) + } else { + Ok(warp::reply::with_status( + "", + warp::http::StatusCode::BAD_REQUEST, + )) + } + } + + let handle_response = warp::path!("confirm" / "response" / u32 / bool) + .and(warp::post()) + .and(with_state(state.clone())) + .and_then(handle_user_response); + + handle_response.or(confirm_dialog) +} + +pub async fn create( + usb_devices: UsbDevices, + notify_tx: mpsc::Sender<()>, + state: Arc, + remembered_allowed_origins: Arc>>, + addr: SocketAddr, +) { // create a warp filter out of "usb_devices" to pass it into our handlers later let usb_devices = warp::any().map(move || (usb_devices.clone(), notify_tx.clone())); @@ -197,34 +329,53 @@ pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: // Only accept some origin // Use untuple_one at the end to get rid of the "unit" return value let check_origin = warp::header::optional("origin") - .and_then(|origin: Option| { - debug!("Origin: {:?}", origin); - async move { - if let Some(origin) = origin { - let scheme_str = origin.scheme_str(); - if scheme_str == Some("chrome-extension") || scheme_str == Some("moz-extension") - { - debug!("Allow Chrome/Firefox extension"); - return Ok(()); - } - match origin.host() { - Some(host) => { - if !is_valid_origin(host) { - warn!("Not whitelisted origin tried to connect: {}", host); + .and(with_state(state.clone())) + .and(with_state(remembered_allowed_origins.clone())) + .and(with_state(addr.clone())) + .and_then( + |origin: Option, + state: Arc, + remembered_allowed_origins, + addr| { + debug!("Origin: {:?}", origin); + async move { + if let Some(origin) = origin { + let scheme_str = origin.scheme_str(); + if scheme_str == Some("chrome-extension") + || scheme_str == Some("moz-extension") + { + debug!("Allow Chrome/Firefox extension"); + return Ok(()); + } + match origin.host() { + Some(host) => { + if !is_valid_origin(host) { + if !user_confirm_origin( + state, + remembered_allowed_origins, + host, + &format!("http://{}", addr), + ) + .await + .unwrap() + { + warn!("Not whitelisted origin tried to connect: {}", host); + return Err(warp::reject::custom(WebError::NonLocalIp)); + } + } + } + None => { + warn!("Not whitelisted origin tried to connect"); return Err(warp::reject::custom(WebError::NonLocalIp)); } } - None => { - warn!("Not whitelisted origin tried to connect"); - return Err(warp::reject::custom(WebError::NonLocalIp)); - } } + // If there is no `origin` header, it must mean that the connection is from + // a website hosted by ourselves. Which is fine. + Ok(()) } - // If there is no `origin` header, it must mean that the connection is from - // a website hosted by ourselves. Which is fine. - Ok(()) - } - }) + }, + ) .untuple_one(); let opt_origin = warp::header::optional("origin"); @@ -266,7 +417,7 @@ pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: let websocket = warp::get() .and(v1_root) .and(websocket) - .and(check_origin) + .and(check_origin.clone()) .and(warp::ws()) .and(usb_devices.clone()) .and_then(ws_upgrade); @@ -275,7 +426,7 @@ pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: let devices = warp::get() .and(v1_root) .and(devices) - .and(check_origin) + .and(check_origin.clone()) .and(usb_devices) .and_then(list_devices) .and(opt_origin) @@ -285,15 +436,16 @@ pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: let info = warp::get() .and(api) .and(info) - .and(check_origin) + .and(check_origin.clone()) .and_then(show_info) .and(opt_origin) .map(add_origin); + let confirm_routes = setup_confirm_routes(state.clone()); // combine routes let routes = only_local_ip .and(only_local_vhost) - .and(websocket.or(devices).or(root).or(info)) + .and(websocket.or(devices).or(root).or(info).or(confirm_routes)) .recover(|err: warp::Rejection| { async { if let Some(err) = err.find::() {