From 743d68383cce6244b8ca7c4c60dcba35bcd76bcc Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Sun, 2 Jun 2024 22:33:25 -0600 Subject: [PATCH 1/2] feat: secure subdomains for settings, improvements to server and login --- Cargo.lock | 24 ++- kinode/packages/settings/pkg/ui/script.js | 3 + kinode/packages/settings/settings/Cargo.toml | 2 +- kinode/packages/settings/settings/src/lib.rs | 6 +- kinode/src/http/login.html | 9 +- kinode/src/http/server.rs | 151 +++++++++---------- kinode/src/http/utils.rs | 31 +++- kinode/src/net/indirect.rs | 18 ++- 8 files changed, 153 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0808e17a9..a04d3d57a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3152,6 +3152,28 @@ dependencies = [ "lib", ] +[[package]] +name = "kinode_process_lib" +version = "0.8.0" +source = "git+https://github.com/kinode-dao/process_lib?rev=7820481#78204816d1a2d5213555655c796950a32403eac6" +dependencies = [ + "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy.git?rev=cad7935)", + "alloy-primitives 0.7.0", + "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy.git?rev=cad7935)", + "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy.git?rev=cad7935)", + "anyhow", + "bincode", + "http 1.1.0", + "mime_guess", + "rand 0.8.5", + "rmp-serde", + "serde", + "serde_json", + "thiserror", + "url", + "wit-bindgen", +] + [[package]] name = "kinode_process_lib" version = "0.8.0" @@ -4922,7 +4944,7 @@ dependencies = [ "anyhow", "base64 0.22.0", "bincode", - "kinode_process_lib 0.8.0 (git+https://github.com/kinode-dao/process_lib?rev=830a86c)", + "kinode_process_lib 0.8.0 (git+https://github.com/kinode-dao/process_lib?rev=7820481)", "rmp-serde", "serde", "serde_json", diff --git a/kinode/packages/settings/pkg/ui/script.js b/kinode/packages/settings/pkg/ui/script.js index 11aecf9ac..740f9ec63 100644 --- a/kinode/packages/settings/pkg/ui/script.js +++ b/kinode/packages/settings/pkg/ui/script.js @@ -22,6 +22,9 @@ function api_call(body) { function shutdown() { api_call("Shutdown"); + setTimeout(() => { + window.location.reload(); + }, 1000); } function populate(data) { diff --git a/kinode/packages/settings/settings/Cargo.toml b/kinode/packages/settings/settings/Cargo.toml index 11f3061f5..1a28877e0 100644 --- a/kinode/packages/settings/settings/Cargo.toml +++ b/kinode/packages/settings/settings/Cargo.toml @@ -10,7 +10,7 @@ simulation-mode = [] anyhow = "1.0" base64 = "0.22.0" bincode = "1.3.3" -kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", rev = "830a86c" } +kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", rev = "7820481" } rmp-serde = "1.2.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/kinode/packages/settings/settings/src/lib.rs b/kinode/packages/settings/settings/src/lib.rs index e461bd58e..26d9d4aa8 100644 --- a/kinode/packages/settings/settings/src/lib.rs +++ b/kinode/packages/settings/settings/src/lib.rs @@ -157,9 +157,9 @@ fn initialize(our: Address) { .unwrap(); // Serve the index.html and other UI files found in pkg/ui at the root path. - http::serve_ui(&our, "ui", true, false, vec!["/"]).unwrap(); - http::bind_http_path("/ask", true, false).unwrap(); - http::bind_ws_path("/", true, false).unwrap(); + http::secure_serve_ui(&our, "ui", vec!["/"]).unwrap(); + http::secure_bind_http_path("/ask").unwrap(); + http::secure_bind_ws_path("/", false).unwrap(); // Grab our state, then enter the main event loop. let mut state: SettingsState = SettingsState::new(our); diff --git a/kinode/src/http/login.html b/kinode/src/http/login.html index 4bcc3c939..9101ac07b 100644 --- a/kinode/src/http/login.html +++ b/kinode/src/http/login.html @@ -1168,7 +1168,7 @@

-

${node}

+

Enter Password
@@ -1193,6 +1193,10 @@

Logging in...

document.getElementById("fake-or-not").innerHTML = "To change your networking info, please restart your node."; } + const host = window.location.host; + const firstSubdomain = host.split('.')[0]; + document.getElementById("node-and-domain").innerText = "${node}: " + firstSubdomain; + async function login(password) { document.getElementById("signup-form").style.display = "none"; document.getElementById("loading").style.display = "flex"; @@ -1207,7 +1211,8 @@

Logging in...

}); if (result.status == 200) { - window.location.href = "/"; + // reload page + window.location.reload(); } else { document.getElementById("signup-form").style.display = "flex"; document.getElementById("loading").style.display = "none"; diff --git a/kinode/src/http/server.rs b/kinode/src/http/server.rs index 97119c2ce..219695bce 100644 --- a/kinode/src/http/server.rs +++ b/kinode/src/http/server.rs @@ -193,7 +193,7 @@ pub async fn http_server( let rpc_bound_path = BoundPath { app: Some(ProcessId::new(Some("rpc"), "distro", "sys")), path: path.clone(), - secure_subdomain: None, // TODO maybe RPC should have subdomain? + secure_subdomain: None, // TODO maybe RPC *should* have subdomain? authenticated: false, local_only: true, static_content: None, @@ -278,14 +278,18 @@ async fn serve( let fake_node = "false"; // filter to receive and handle login requests - let login_html: &'static str = LOGIN_HTML - .replace("${node}", &our) - .replace("${fake}", fake_node) - .leak(); + let login_html: Arc = Arc::new( + LOGIN_HTML + .replace("${node}", &our) + .replace("${fake}", fake_node), + ); let cloned_our = our.clone(); + let cloned_login_html: &'static str = login_html.to_string().leak(); let login = warp::path("login").and(warp::path::end()).and( warp::get() - .map(move || warp::reply::with_status(warp::reply::html(login_html), StatusCode::OK)) + .map(move || { + warp::reply::with_status(warp::reply::html(cloned_login_html), StatusCode::OK) + }) .or(warp::post() .and(warp::body::content_length_limit(1024 * 16)) .and(warp::body::json()) @@ -308,6 +312,7 @@ async fn serve( .and(warp::any().map(move || jwt_secret_bytes.clone())) .and(warp::any().map(move || send_to_loop.clone())) .and(warp::any().map(move || print_tx.clone())) + .and(warp::any().map(move || login_html.clone())) .and_then(http_handler); let filter_with_ws = ws_route.or(login).or(filter); @@ -318,7 +323,6 @@ async fn serve( /// handle non-GET requests on /login. if POST, validate password /// and return auth token, which will be stored in a cookie. -/// then redirect to wherever they were trying to go. async fn login_handler( info: LoginInfo, our: Arc, @@ -381,7 +385,7 @@ async fn ws_handler( send_to_loop: MessageSender, print_tx: PrintSender, ) -> Result { - let original_path = normalize_path(path.as_str()); + let original_path = normalize_path(path.as_str()).to_string(); let _ = print_tx .send(Printout { verbosity: 2, @@ -479,6 +483,7 @@ async fn http_handler( jwt_secret_bytes: Arc>, send_to_loop: MessageSender, print_tx: PrintSender, + login_html: Arc, ) -> Result { // trim trailing "/" let original_path = normalize_path(path.as_str()); @@ -524,18 +529,13 @@ async fn http_handler( }) .await; return Ok(warp::http::Response::builder() - .status(StatusCode::TEMPORARY_REDIRECT) - .header( - "Location", - format!( - "http://{}/login", - host.unwrap_or(warp::host::Authority::from_static("localhost")) - ), - ) - .body(vec![]) + .status(StatusCode::OK) + .body(login_html.to_string()) .into_response()); } + let host = host.unwrap_or(warp::host::Authority::from_static("localhost")); + if let Some(ref subdomain) = bound_path.secure_subdomain { let _ = print_tx .send(Printout { @@ -545,15 +545,33 @@ async fn http_handler( ), }) .await; + let request_subdomain = host.host().split('.').next().unwrap_or(""); // 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()); + if request_subdomain.is_empty() { + return Ok(warp::reply::with_status( + "attempted to access secure subdomain without host", + StatusCode::UNAUTHORIZED, + ) + .into_response()); } - 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 { - return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response()); + return Ok(warp::http::Response::builder() + .status(StatusCode::TEMPORARY_REDIRECT) + .header( + "Location", + format!( + "{}://{}.{}{}", + match headers.get("X-Forwarded-Proto") { + Some(proto) => proto.to_str().unwrap_or("http"), + None => "http", + }, + subdomain, + host, + original_path, + ), + ) + .body(vec![]) + .into_response()); } } @@ -618,8 +636,8 @@ async fn http_handler( source_socket_addr: socket_addr.map(|addr| addr.to_string()), method: method.to_string(), url: format!( - "http://{}{}", - host.unwrap_or(warp::host::Authority::from_static("localhost")), + "http://{}{}", // note that protocol is being lost here + host.host(), original_path ), bound_path: bound_path.path.clone(), @@ -657,7 +675,7 @@ async fn http_handler( } let (response_sender, response_receiver) = tokio::sync::oneshot::channel(); - http_response_senders.insert(id, (original_path, response_sender)); + http_response_senders.insert(id, (original_path.to_string(), response_sender)); match send_to_loop.send(message).await { Ok(_) => {} @@ -1126,19 +1144,13 @@ async fn handle_app_message( }; match message { HttpServerAction::Bind { - mut path, + path, authenticated, local_only, cache, } => { + let path = format_path_with_process(&km.source.process, &path); let mut path_bindings = path_bindings.write().await; - if km.source.process != "homepage:homepage:sys" { - path = if path.starts_with('/') { - format!("/{}{}", km.source.process, path) - } else { - format!("/{}/{}", km.source.process, path) - }; - } let _ = print_tx .send(Printout { verbosity: 2, @@ -1157,7 +1169,7 @@ async fn handle_app_message( if !cache { // trim trailing "/" path_bindings.add( - &normalize_path(&path), + &path, BoundPath { app: Some(km.source.process.clone()), path: path.clone(), @@ -1180,7 +1192,7 @@ async fn handle_app_message( }; // trim trailing "/" path_bindings.add( - &normalize_path(&path), + &path, BoundPath { app: Some(km.source.process.clone()), path: path.clone(), @@ -1193,18 +1205,21 @@ async fn handle_app_message( } } 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(32).0.to_owned(); + let path = format_path_with_process(&km.source.process, &path); + let subdomain = generate_secure_subdomain(&km.source.process); let mut path_bindings = path_bindings.write().await; + let _ = print_tx + .send(Printout { + verbosity: 2, + content: format!( + "http: binding subdomain {subdomain} with path {path}, {}", + if cache { "cached" } else { "dynamic" }, + ), + }) + .await; if !cache { - // trim trailing "/" path_bindings.add( - &normalize_path(&path), + &path, BoundPath { app: Some(km.source.process.clone()), path: path.clone(), @@ -1227,7 +1242,7 @@ async fn handle_app_message( }; // trim trailing "/" path_bindings.add( - &normalize_path(&path), + &path, BoundPath { app: Some(km.source.process.clone()), path: path.clone(), @@ -1239,17 +1254,11 @@ async fn handle_app_message( ); } } - HttpServerAction::Unbind { mut path } => { + HttpServerAction::Unbind { path } => { + let path = format_path_with_process(&km.source.process, &path); let mut path_bindings = path_bindings.write().await; - if km.source.process != "homepage:homepage:sys" { - path = if path.starts_with('/') { - format!("/{}{}", km.source.process, path) - } else { - format!("/{}/{}", km.source.process, path) - }; - } path_bindings.add( - &normalize_path(&path), + &path, BoundPath { app: None, path: path.clone(), @@ -1261,19 +1270,15 @@ async fn handle_app_message( ); } HttpServerAction::WebSocketBind { - mut path, + path, authenticated, encrypted, extension, } => { - path = if path.starts_with('/') { - format!("/{}{}", km.source.process, path) - } else { - format!("/{}/{}", km.source.process, path) - }; + let path = format_path_with_process(&km.source.process, &path); let mut ws_path_bindings = ws_path_bindings.write().await; ws_path_bindings.add( - &normalize_path(&path), + &path, BoundWsPath { app: Some(km.source.process.clone()), secure_subdomain: None, @@ -1284,21 +1289,15 @@ async fn handle_app_message( ); } HttpServerAction::WebSocketSecureBind { - mut path, + path, encrypted, extension, } => { - path = if path.starts_with('/') { - format!("/{}{}", km.source.process, path) - } else { - format!("/{}/{}", km.source.process, path) - }; - let process_id_hash = - format!("{:x}", Sha256::digest(km.source.process.to_string())); - let subdomain = process_id_hash.split_at(32).0.to_owned(); + let path = format_path_with_process(&km.source.process, &path); + let subdomain = generate_secure_subdomain(&km.source.process); let mut ws_path_bindings = ws_path_bindings.write().await; ws_path_bindings.add( - &normalize_path(&path), + &path, BoundWsPath { app: Some(km.source.process.clone()), secure_subdomain: Some(subdomain), @@ -1309,14 +1308,10 @@ async fn handle_app_message( ); } HttpServerAction::WebSocketUnbind { mut path } => { + let path = format_path_with_process(&km.source.process, &path); let mut ws_path_bindings = ws_path_bindings.write().await; - path = if path.starts_with('/') { - format!("/{}{}", km.source.process, path) - } else { - format!("/{}/{}", km.source.process, path) - }; ws_path_bindings.add( - &normalize_path(&path), + &path, BoundWsPath { app: None, secure_subdomain: None, diff --git a/kinode/src/http/utils.rs b/kinode/src/http/utils.rs index ebb44b735..1bcbe380d 100644 --- a/kinode/src/http/utils.rs +++ b/kinode/src/http/utils.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use tokio::net::TcpListener; use warp::http::{header::HeaderName, header::HeaderValue, HeaderMap}; -use lib::types::http_server::*; +use lib::{core::ProcessId, types::http_server::*}; #[derive(Serialize, Deserialize)] pub struct RpcMessage { @@ -66,13 +66,36 @@ pub fn auth_cookie_valid(our_node: &str, cookie: &str, jwt_secret: &[u8]) -> boo } } -pub fn normalize_path(path: &str) -> String { +pub fn normalize_path(path: &str) -> &str { match path.strip_suffix('/') { - Some(new) => new.to_string(), - None => path.to_string(), + Some(new) => new, + None => path, } } +pub fn format_path_with_process(process: &ProcessId, path: &str) -> String { + let process = process.to_string(); + if process != "homepage:homepage:sys" { + if path.starts_with('/') { + format!("/{}{}", process, normalize_path(path)) + } else { + format!("/{}/{}", process, normalize_path(path)) + } + } else { + normalize_path(path).to_string() + } +} + +/// first strip the process name leaving just package ID, then +/// convert all non-alphanumeric characters in the process ID to `-` +pub fn generate_secure_subdomain(process: &ProcessId) -> String { + [process.package(), process.publisher()] + .join("-") + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) + .collect() +} + pub fn serialize_headers(headers: &HeaderMap) -> HashMap { let mut hashmap = HashMap::new(); for (key, value) in headers.iter() { diff --git a/kinode/src/net/indirect.rs b/kinode/src/net/indirect.rs index e062e7c0b..1fc911203 100644 --- a/kinode/src/net/indirect.rs +++ b/kinode/src/net/indirect.rs @@ -40,7 +40,14 @@ pub async fn connect_to_router(router_id: &Identity, ext: &IdentityExt, data: &N ); if let Some((_ip, port)) = router_id.tcp_routing() { match tcp::init_direct(ext, data, &router_id, *port, true, peer_rx).await { - Ok(()) => return, + Ok(()) => { + utils::print_debug( + &ext.print_tx, + &format!("net: connected to router {} via tcp", router_id.name), + ) + .await; + return; + } Err(peer_rx) => { return connect::handle_failed_connection(ext, data, router_id, peer_rx).await; } @@ -48,7 +55,14 @@ pub async fn connect_to_router(router_id: &Identity, ext: &IdentityExt, data: &N } if let Some((_ip, port)) = router_id.ws_routing() { match ws::init_direct(ext, data, &router_id, *port, true, peer_rx).await { - Ok(()) => return, + Ok(()) => { + utils::print_debug( + &ext.print_tx, + &format!("net: connected to router {} via ws", router_id.name), + ) + .await; + return; + } Err(peer_rx) => { return connect::handle_failed_connection(ext, data, router_id, peer_rx).await; } From fea0799771eeea705c351a63cec07b688c2532d8 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Mon, 3 Jun 2024 16:44:27 -0600 Subject: [PATCH 2/2] fix: make tokens for subdomains unique/linked --- kinode/src/http/login.html | 7 +- kinode/src/http/server.rs | 190 +++++++++++++++++++---------------- kinode/src/http/utils.rs | 28 ++++-- kinode/src/keygen.rs | 20 ++-- kinode/src/register.rs | 8 +- lib/src/core.rs | 1 + lib/src/http/server_types.rs | 1 + 7 files changed, 142 insertions(+), 113 deletions(-) diff --git a/kinode/src/http/login.html b/kinode/src/http/login.html index 9101ac07b..fb3d13735 100644 --- a/kinode/src/http/login.html +++ b/kinode/src/http/login.html @@ -1193,9 +1193,8 @@

Logging in...

document.getElementById("fake-or-not").innerHTML = "To change your networking info, please restart your node."; } - const host = window.location.host; - const firstSubdomain = host.split('.')[0]; - document.getElementById("node-and-domain").innerText = "${node}: " + firstSubdomain; + const firstPathItem = window.location.pathname.split('/')[1]; + document.getElementById("node-and-domain").innerText = "${node} " + firstPathItem; async function login(password) { document.getElementById("signup-form").style.display = "none"; @@ -1207,7 +1206,7 @@

Logging in...

const result = await fetch("/login", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ password_hash: hashHex }), + body: JSON.stringify({ password_hash: hashHex, subdomain: firstPathItem }), }); if (result.status == 200) { diff --git a/kinode/src/http/server.rs b/kinode/src/http/server.rs index 219695bce..936c2ef02 100644 --- a/kinode/src/http/server.rs +++ b/kinode/src/http/server.rs @@ -250,7 +250,7 @@ async fn serve( let _ = print_tx .send(Printout { verbosity: 0, - content: format!("http_server: running on port {}", our_port), + content: format!("http_server: running on port {our_port}"), }) .await; @@ -331,16 +331,21 @@ async fn login_handler( #[cfg(feature = "simulation-mode")] let info = LoginInfo { password_hash: "secret".to_string(), + subdomain: info.subdomain, }; match keygen::decode_keyfile(&encoded_keyfile, &info.password_hash) { Ok(keyfile) => { - let token = match keygen::generate_jwt(&keyfile.jwt_secret_bytes, our.as_ref()) { + let token = match keygen::generate_jwt( + &keyfile.jwt_secret_bytes, + our.as_ref(), + &info.subdomain, + ) { Some(token) => token, None => { return Ok(warp::reply::with_status( warp::reply::json(&"Failed to generate JWT"), - StatusCode::SERVICE_UNAVAILABLE, + StatusCode::INTERNAL_SERVER_ERROR, ) .into_response()) } @@ -352,20 +357,25 @@ async fn login_handler( ) .into_response(); - match HeaderValue::from_str(&format!("kinode-auth_{}={};", our.as_ref(), &token)) { + let cookie = match info.subdomain.unwrap_or_default().as_str() { + "" => format!("kinode-auth_{our}={token};"), + subdomain => format!("kinode-auth_{our}@{subdomain}={token};"), + }; + + match HeaderValue::from_str(&cookie) { Ok(v) => { response.headers_mut().append("set-cookie", v); Ok(response) } - Err(_) => Ok(warp::reply::with_status( - warp::reply::json(&"Failed to generate Auth JWT"), + Err(e) => Ok(warp::reply::with_status( + warp::reply::json(&format!("Failed to generate Auth JWT: {e}")), StatusCode::INTERNAL_SERVER_ERROR, ) .into_response()), } } - Err(_) => Ok(warp::reply::with_status( - warp::reply::json(&"Failed to decode keyfile"), + Err(e) => Ok(warp::reply::with_status( + warp::reply::json(&format!("Failed to decode keyfile: {e}")), StatusCode::INTERNAL_SERVER_ERROR, ) .into_response()), @@ -405,33 +415,36 @@ async fn ws_handler( return Err(warp::reject::not_found()); }; - if let Some(ref subdomain) = bound_path.secure_subdomain { - let _ = print_tx - .send(Printout { - verbosity: 2, - content: format!( - "http_server: ws request for {original_path} bound by subdomain {subdomain}" - ), - }) - .await; - // assert that host matches what this app wants it to be - if host.is_none() { - return Err(warp::reject::not_found()); - } - 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 { - return Err(warp::reject::not_found()); - } - } - if bound_path.authenticated { let Some(auth_token) = serialized_headers.get("cookie") else { return Err(warp::reject::not_found()); }; - if !auth_cookie_valid(&our, auth_token, &jwt_secret_bytes) { - return Err(warp::reject::not_found()); + + if let Some(ref subdomain) = bound_path.secure_subdomain { + let _ = print_tx + .send(Printout { + verbosity: 2, + content: format!( + "http_server: ws request for {original_path} bound by subdomain {subdomain}" + ), + }) + .await; + // assert that host matches what this app wants it to be + let host = match host { + Some(host) => host, + None => return Err(warp::reject::not_found()), + }; + // parse out subdomain from host (there can only be one) + let request_subdomain = host.host().split('.').next().unwrap_or(""); + if request_subdomain != subdomain + || !auth_cookie_valid(&our, Some(&app), auth_token, &jwt_secret_bytes) + { + return Err(warp::reject::not_found()); + } + } else { + if !auth_cookie_valid(&our, None, auth_token, &jwt_secret_bytes) { + return Err(warp::reject::not_found()); + } } } @@ -512,66 +525,71 @@ async fn http_handler( return Ok(warp::reply::with_status(vec![], StatusCode::NOT_FOUND).into_response()); }; - if bound_path.authenticated - && !auth_cookie_valid( - &our, - serialized_headers.get("cookie").unwrap_or(&"".to_string()), - &jwt_secret_bytes, - ) - { - // redirect to login page so they can get an auth token - let _ = print_tx - .send(Printout { - verbosity: 2, - content: format!( - "http_server: redirecting request from {socket_addr:?} to login page" - ), - }) - .await; - return Ok(warp::http::Response::builder() - .status(StatusCode::OK) - .body(login_html.to_string()) - .into_response()); - } - let host = host.unwrap_or(warp::host::Authority::from_static("localhost")); - if let Some(ref subdomain) = bound_path.secure_subdomain { - let _ = print_tx - .send(Printout { - verbosity: 2, - content: format!( - "http_server: request for {original_path} bound by subdomain {subdomain}" - ), - }) - .await; - let request_subdomain = host.host().split('.').next().unwrap_or(""); - // assert that host matches what this app wants it to be - if request_subdomain.is_empty() { - return Ok(warp::reply::with_status( - "attempted to access secure subdomain without host", - StatusCode::UNAUTHORIZED, - ) - .into_response()); - } - if request_subdomain != subdomain { - return Ok(warp::http::Response::builder() - .status(StatusCode::TEMPORARY_REDIRECT) - .header( - "Location", - format!( - "{}://{}.{}{}", - match headers.get("X-Forwarded-Proto") { - Some(proto) => proto.to_str().unwrap_or("http"), - None => "http", - }, - subdomain, - host, - original_path, + if bound_path.authenticated { + if let Some(ref subdomain) = bound_path.secure_subdomain { + let _ = print_tx + .send(Printout { + verbosity: 2, + content: format!( + "http_server: request for {original_path} bound by subdomain {subdomain}" ), + }) + .await; + let request_subdomain = host.host().split('.').next().unwrap_or(""); + // assert that host matches what this app wants it to be + if request_subdomain.is_empty() { + return Ok(warp::reply::with_status( + "attempted to access secure subdomain without host", + StatusCode::UNAUTHORIZED, ) - .body(vec![]) .into_response()); + } + if request_subdomain != subdomain { + return Ok(warp::http::Response::builder() + .status(StatusCode::TEMPORARY_REDIRECT) + .header( + "Location", + format!( + "{}://{}.{}{}", + match headers.get("X-Forwarded-Proto") { + Some(proto) => proto.to_str().unwrap_or("http"), + None => "http", + }, + subdomain, + host, + original_path, + ), + ) + .body(vec![]) + .into_response()); + } + if !auth_cookie_valid( + &our, + Some(&app), + serialized_headers.get("cookie").unwrap_or(&"".to_string()), + &jwt_secret_bytes, + ) { + // redirect to login page so they can get an auth token + return Ok(warp::http::Response::builder() + .status(StatusCode::OK) + .body(login_html.to_string()) + .into_response()); + } + } else { + if !auth_cookie_valid( + &our, + None, + serialized_headers.get("cookie").unwrap_or(&"".to_string()), + &jwt_secret_bytes, + ) { + // redirect to login page so they can get an auth token + return Ok(warp::http::Response::builder() + .status(StatusCode::OK) + .body(login_html.to_string()) + .into_response()); + } } } diff --git a/kinode/src/http/utils.rs b/kinode/src/http/utils.rs index 1bcbe380d..0dfb86ae8 100644 --- a/kinode/src/http/utils.rs +++ b/kinode/src/http/utils.rs @@ -35,16 +35,24 @@ pub fn _verify_auth_token(auth_token: &str, jwt_secret: &[u8]) -> Result bool { - let cookie_parts: Vec<&str> = cookie.split("; ").collect(); - let mut auth_token = None; +pub fn auth_cookie_valid( + our_node: &str, + subdomain: Option<&ProcessId>, + cookie: &str, + jwt_secret: &[u8], +) -> bool { + let cookie: Vec<&str> = cookie.split("; ").collect(); + + let token_label = match subdomain { + None => format!("kinode-auth_{our_node}"), + Some(subdomain) => format!("kinode-auth_{our_node}@{subdomain}"), + }; - for cookie_part in cookie_parts { - let cookie_part_parts: Vec<&str> = cookie_part.split('=').collect(); - if cookie_part_parts.len() == 2 - && cookie_part_parts[0] == format!("kinode-auth_{}", our_node) - { - auth_token = Some(cookie_part_parts[1].to_string()); + let mut auth_token = None; + for entry in cookie { + let cookie_parts: Vec<&str> = entry.split('=').collect(); + if cookie_parts.len() == 2 && cookie_parts[0] == token_label { + auth_token = Some(cookie_parts[1].to_string()); break; } } @@ -61,7 +69,7 @@ pub fn auth_cookie_valid(our_node: &str, cookie: &str, jwt_secret: &[u8]) -> boo let claims: Result = auth_token.verify_with_key(&secret); match claims { - Ok(data) => data.username == our_node, + Ok(data) => data.username == our_node && data.subdomain == subdomain.map(|s| s.to_string()), Err(_) => false, } } diff --git a/kinode/src/keygen.rs b/kinode/src/keygen.rs index 3e7d40e0d..7b71443cf 100644 --- a/kinode/src/keygen.rs +++ b/kinode/src/keygen.rs @@ -112,21 +112,25 @@ pub fn decode_keyfile(keyfile: &[u8], password: &str) -> Result Option { - let jwt_secret: Hmac = match Hmac::new_from_slice(jwt_secret_bytes) { - Ok(secret) => secret, - Err(_) => return None, +pub fn generate_jwt( + jwt_secret_bytes: &[u8], + username: &str, + subdomain: &Option, +) -> Option { + let jwt_secret: Hmac = Hmac::new_from_slice(jwt_secret_bytes).ok()?; + + let subdomain = match subdomain.clone().unwrap_or_default().as_str() { + "" => None, + subdomain => Some(subdomain.to_string()), }; let claims = crate::http::server_types::JwtClaims { username: username.to_string(), + subdomain, expiration: 0, }; - match claims.sign_with_key(&jwt_secret) { - Ok(token) => Some(token), - Err(_) => None, - } + claims.sign_with_key(&jwt_secret).ok() } #[cfg(not(feature = "simulation-mode"))] diff --git a/kinode/src/register.rs b/kinode/src/register.rs index b83c20abe..5c6b5f54f 100644 --- a/kinode/src/register.rs +++ b/kinode/src/register.rs @@ -785,7 +785,7 @@ async fn success_response( encoded_keyfile: Vec, ) -> Result { let encoded_keyfile_str = base64_standard.encode(&encoded_keyfile); - let token = match keygen::generate_jwt(&decoded_keyfile.jwt_secret_bytes, &our.name) { + let token = match keygen::generate_jwt(&decoded_keyfile.jwt_secret_bytes, &our.name, &None) { Some(token) => token, None => { return Ok(warp::reply::with_status( @@ -805,11 +805,9 @@ async fn success_response( warp::reply::with_status(warp::reply::json(&encoded_keyfile_str), StatusCode::FOUND) .into_response(); - let headers = response.headers_mut(); - - match HeaderValue::from_str(&format!("kinode-auth_{}={};", &our.name, &token)) { + match HeaderValue::from_str(&format!("kinode-auth_{}={token};", our.name)) { Ok(v) => { - headers.append(SET_COOKIE, v); + response.headers_mut().append(SET_COOKIE, v); } Err(_) => { return Ok(warp::reply::with_status( diff --git a/lib/src/core.rs b/lib/src/core.rs index f65607740..abe202e92 100644 --- a/lib/src/core.rs +++ b/lib/src/core.rs @@ -993,6 +993,7 @@ pub struct ImportKeyfileInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoginInfo { pub password_hash: String, + pub subdomain: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/lib/src/http/server_types.rs b/lib/src/http/server_types.rs index d35dbc7a7..7d363c056 100644 --- a/lib/src/http/server_types.rs +++ b/lib/src/http/server_types.rs @@ -194,5 +194,6 @@ pub struct WsRegisterResponse { #[derive(Debug, Serialize, Deserialize)] pub struct JwtClaims { pub username: String, + pub subdomain: Option, pub expiration: u64, }