From 5011b5aeb2d8972df7ce3e27ea3a739451508c2e Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Tue, 5 Dec 2023 17:03:27 -0500 Subject: [PATCH 01/13] server: add SecureBind action, terminal: print verbosity level with printout --- src/http/server.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++ src/http/types.rs | 20 +++++++++++++ src/terminal/mod.rs | 8 +++++- 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/http/server.rs b/src/http/server.rs index 3608ea1a..0137c144 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -5,7 +5,9 @@ use crate::types::*; use anyhow::Result; use dashmap::DashMap; use futures::{SinkExt, StreamExt}; +use http::uri::Authority; use route_recognizer::Router; +use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; @@ -32,6 +34,7 @@ type PathBindings = Arc>>; struct BoundPath { pub app: ProcessId, + pub secure_subdomain: Option, pub authenticated: bool, pub local_only: bool, pub static_content: Option, // TODO store in filesystem and cache @@ -69,6 +72,7 @@ pub async fn http_server( let mut bindings_map: Router = Router::new(); let rpc_bound_path = BoundPath { app: ProcessId::from_str("rpc:sys:uqbar").unwrap(), + secure_subdomain: None, // TODO maybe RPC should have subdomain? authenticated: false, local_only: true, static_content: None, @@ -158,6 +162,7 @@ async fn serve( // Filter to receive HTTP requests let filter = warp::filters::method::method() .and(warp::addr::remote()) + .and(warp::filters::host::optional()) .and(warp::path::full()) .and(warp::filters::header::headers_cloned()) .and(warp::filters::body::bytes()) @@ -177,6 +182,7 @@ async fn serve( async fn http_handler( method: warp::http::Method, socket_addr: Option, + host: Option, path: warp::path::FullPath, headers: warp::http::HeaderMap, body: warp::hyper::body::Bytes, @@ -200,6 +206,7 @@ async fn http_handler( let bound_path = route.handler(); if bound_path.authenticated { + println!("got request for path that requires auth\r"); let auth_token = serialized_headers .get("cookie") .cloned() @@ -208,6 +215,22 @@ async fn http_handler( return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response()); } } + println!("subdomain request passed the vibe check #1\r"); + + if let Some(ref subdomain) = bound_path.secure_subdomain { + println!("got request for path bound by subdomain {:?}: {}\r", path, subdomain); + // assert that host matches what this app wants it to be + if host.is_none() { + return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response()); + } + let host = host.unwrap(); + // parse out subdomain from host (there can only be one) + let request_subdomain = host.host().split('.').next().unwrap_or(""); + if request_subdomain != subdomain { + return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response()); + } + } + println!("subdomain request passed the vibe check #2\r"); let is_local = socket_addr .map(|addr| addr.ip().is_loopback()) @@ -722,6 +745,7 @@ async fn handle_app_message( &normalize_path(&path), BoundPath { app: km.source.process.clone(), + secure_subdomain: None, authenticated, local_only, static_content: None, @@ -743,6 +767,7 @@ async fn handle_app_message( &normalize_path(&path), BoundPath { app: km.source.process.clone(), + secure_subdomain: None, authenticated, local_only, static_content: Some(payload), @@ -751,6 +776,49 @@ async fn handle_app_message( } send_action_response(km.id, km.source, &send_to_loop, Ok(())).await; } + HttpServerAction::SecureBind { path, cache } => { + let process_id_hash = + format!("{:x}", Sha256::digest(km.source.process.to_string())); + let subdomain = process_id_hash.split_at(16).0.to_owned(); + println!("generated secure subdomain for {}: {}\r", km.source.process, subdomain); + let mut path_bindings = path_bindings.write().await; + if !cache { + // trim trailing "/" + path_bindings.add( + &normalize_path(&path), + BoundPath { + app: km.source.process.clone(), + secure_subdomain: Some(subdomain), + authenticated: true, + local_only: false, + static_content: None, + }, + ); + } else { + let Some(payload) = km.payload else { + send_action_response( + km.id, + km.source, + &send_to_loop, + Err(HttpServerError::NoPayload), + ) + .await; + return; + }; + // trim trailing "/" + path_bindings.add( + &normalize_path(&path), + BoundPath { + app: km.source.process.clone(), + secure_subdomain: Some(subdomain), + authenticated: true, + local_only: false, + static_content: Some(payload), + }, + ); + } + unimplemented!(); + } HttpServerAction::WebSocketOpen(_) => { // we cannot receive these, only send them to processes send_action_response( diff --git a/src/http/types.rs b/src/http/types.rs index ecba6711..fdc0fdc2 100644 --- a/src/http/types.rs +++ b/src/http/types.rs @@ -67,8 +67,28 @@ pub enum HttpServerAction { /// be the static file to serve at this path. Bind { path: String, + /// Set whether the HTTP request needs a valid login cookie, AKA, whether + /// the user needs to be logged in to access this path. authenticated: bool, + /// Set whether requests can be fielded from anywhere, or only the loopback address. local_only: bool, + /// Set whether to bind the payload statically to this path. That is, take the + /// payload bytes and serve them as the response to any request to this path. + cache: bool, + }, + /// SecureBind expects a payload if and only if `cache` is TRUE. The payload should + /// be the static file to serve at this path. + /// + /// SecureBind is the same as Bind, except that it forces requests to be made from + /// the unique subdomain of the process that bound the path. These requests are + /// *always* authenticated, and *never* local_only. The purpose of SecureBind is to + /// serve elements of an app frontend or API in an exclusive manner, such that other + /// apps installed on this node cannot access them. Since the subdomain is unique, it + /// will require the user to be logged in separately to the general domain authentication. + SecureBind { + path: String, + /// Set whether to bind the payload statically to this path. That is, take the + /// payload bytes and serve them as the response to any request to this path. cache: bool, }, /// Processes will RECEIVE this kind of request when a client connects to them. diff --git a/src/terminal/mod.rs b/src/terminal/mod.rs index 74bd7ef7..2ddfa3f4 100644 --- a/src/terminal/mod.rs +++ b/src/terminal/mod.rs @@ -146,7 +146,13 @@ pub async fn terminal( stdout, cursor::MoveTo(0, win_rows - 1), terminal::Clear(ClearType::CurrentLine), - Print(format!("{} {}/{} {:02}:{:02} ", + Print(format!("{}{} {}/{} {:02}:{:02} ", + match printout.verbosity { + 0 => "", + 1 => "1️⃣ ", + 2 => "2️⃣ ", + _ => "3️⃣ ", + }, now.weekday(), now.month(), now.day(), From 6aa3009366dadd87db2702d65ddc87e405142b31 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 22:03:58 +0000 Subject: [PATCH 02/13] Format Rust code using rustfmt --- src/http/server.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/http/server.rs b/src/http/server.rs index 0137c144..a3462053 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -218,7 +218,10 @@ async fn http_handler( println!("subdomain request passed the vibe check #1\r"); if let Some(ref subdomain) = bound_path.secure_subdomain { - println!("got request for path bound by subdomain {:?}: {}\r", path, subdomain); + println!( + "got request for path bound by subdomain {:?}: {}\r", + path, subdomain + ); // assert that host matches what this app wants it to be if host.is_none() { return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response()); @@ -780,7 +783,10 @@ async fn handle_app_message( let process_id_hash = format!("{:x}", Sha256::digest(km.source.process.to_string())); let subdomain = process_id_hash.split_at(16).0.to_owned(); - println!("generated secure subdomain for {}: {}\r", km.source.process, subdomain); + println!( + "generated secure subdomain for {}: {}\r", + km.source.process, subdomain + ); let mut path_bindings = path_bindings.write().await; if !cache { // trim trailing "/" From 3fa96c89a23457bec16069177cff59181ee775ce Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Wed, 6 Dec 2023 13:17:47 -0500 Subject: [PATCH 03/13] secure subdomains: use 32 chars from hash --- src/http/server.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/http/server.rs b/src/http/server.rs index a3462053..206a40a1 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -780,9 +780,13 @@ async fn handle_app_message( send_action_response(km.id, km.source, &send_to_loop, Ok(())).await; } HttpServerAction::SecureBind { path, cache } => { + // the process ID is hashed to generate a unique subdomain + // only the first 32 chars, or 128 bits are used. + // we hash because the process ID can contain many more than + // simply alphanumeric characters that will cause issues as a subdomain. let process_id_hash = format!("{:x}", Sha256::digest(km.source.process.to_string())); - let subdomain = process_id_hash.split_at(16).0.to_owned(); + let subdomain = process_id_hash.split_at(32).0.to_owned(); println!( "generated secure subdomain for {}: {}\r", km.source.process, subdomain From dfb544c503d3cedbcf15622f8ee197352cd9caa8 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Wed, 6 Dec 2023 13:23:03 -0500 Subject: [PATCH 04/13] use real host in raw_path --- src/http/server.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/http/server.rs b/src/http/server.rs index 206a40a1..67138ac7 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -226,7 +226,7 @@ async fn http_handler( if host.is_none() { return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response()); } - let host = host.unwrap(); + let host = host.as_ref().unwrap(); // parse out subdomain from host (there can only be one) let request_subdomain = host.host().split('.').next().unwrap_or(""); if request_subdomain != subdomain { @@ -287,7 +287,12 @@ async fn http_handler( ipc: serde_json::to_vec(&IncomingHttpRequest { source_socket_addr: socket_addr.map(|addr| addr.to_string()), method: method.to_string(), - raw_path: format!("http://localhost{}", original_path), + raw_path: format!( + "{}{}", + host.unwrap_or(Authority::from_static("localhost")) + .to_string(), + original_path + ), headers: serialized_headers, }) .unwrap(), From c0cca43d5488bdf9952ef14e854e2a1dec8b3f28 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Thu, 7 Dec 2023 00:06:17 -0500 Subject: [PATCH 05/13] http_server: types change, single request type and query_params in request --- modules/homepage/src/home.html | 2 +- src/http/server.rs | 210 ++++++++++++++++++++------------- src/http/types.rs | 31 +++-- src/main.rs | 15 +-- 4 files changed, 159 insertions(+), 99 deletions(-) diff --git a/modules/homepage/src/home.html b/modules/homepage/src/home.html index 20a16cd5..fa7be274 100644 --- a/modules/homepage/src/home.html +++ b/modules/homepage/src/home.html @@ -196,7 +196,7 @@

Apps:

- Chess [NOT WORKING] + Chess + + + + From bcd47c2d2e75a7f6084db5c423f3dce29f39aae9 Mon Sep 17 00:00:00 2001 From: Will Galebach Date: Thu, 7 Dec 2023 15:15:48 +0000 Subject: [PATCH 08/13] login.html updates --- src/http/login.html | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/http/login.html b/src/http/login.html index e743fd82..b9c3e633 100644 --- a/src/http/login.html +++ b/src/http/login.html @@ -24,19 +24,20 @@
- - -
+ +
+
-