diff --git a/Cargo.toml b/Cargo.toml index a4407b59..d76c5426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,10 +59,10 @@ serde_json = "1.0" serde_urlencoded = "0.7" sha2 = "0.10" snow = { version = "0.9.3", features = ["ring-resolver"] } -thiserror = "1.0.43" +thiserror = "1.0" tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "sync"] } tokio-tungstenite = "*" -url = "*" +url = "2.4.1" uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" } uuid = { version = "1.1.2", features = ["serde", "v4"] } warp = "0.3.5" diff --git a/modules/app_store/app_store/src/lib.rs b/modules/app_store/app_store/src/lib.rs index 767550bc..efff9d61 100644 --- a/modules/app_store/app_store/src/lib.rs +++ b/modules/app_store/app_store/src/lib.rs @@ -380,7 +380,7 @@ fn handle_local_request( })?) .send_and_await_response(5)??; let Some(payload) = get_payload() else { - return Err(anyhow::anyhow!("no metadata payload")); + return Err(anyhow::anyhow!("no metadata found!")); }; let metadata = String::from_utf8(payload.bytes)?; let metadata = serde_json::from_str::(&metadata)?; diff --git a/modules/chess/Cargo.lock b/modules/chess/Cargo.lock index 81f03c91..c727ecee 100644 --- a/modules/chess/Cargo.lock +++ b/modules/chess/Cargo.lock @@ -50,6 +50,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + [[package]] name = "cfg-if" version = "1.0.0" @@ -124,6 +130,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -162,12 +183,33 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "id-arena" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "2.1.0" @@ -234,6 +276,12 @@ dependencies = [ "libc", ] +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + [[package]] name = "pleco" version = "0.5.0" @@ -512,12 +560,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.10.1" @@ -532,16 +630,31 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "uqbar_process_lib" -version = "0.2.0" -source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=e53c124#e53c124ec95ef99c06d201d4d08dada8ec691d29" +version = "0.3.0" +source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=ec0933f#ec0933fe48f5e3e415947a73cd33c3da3ce22c33" dependencies = [ "anyhow", "bincode", + "http", "rand 0.8.5", "serde", + "serde_json", + "thiserror", + "url", "wit-bindgen", ] +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/modules/chess/Cargo.toml b/modules/chess/Cargo.toml index f85b48df..59cef4a2 100644 --- a/modules/chess/Cargo.toml +++ b/modules/chess/Cargo.toml @@ -17,7 +17,7 @@ bincode = "1.3.3" pleco = "0.5" serde = {version = "1.0", features = ["derive"] } serde_json = "1.0" -uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" } +uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "ec0933f" } wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "5390bab780733f1660d14c254ec985df2816bf1d" } [lib] diff --git a/modules/chess/pkg/manifest.json b/modules/chess/pkg/manifest.json index c8139fbc..e8c1751c 100644 --- a/modules/chess/pkg/manifest.json +++ b/modules/chess/pkg/manifest.json @@ -5,8 +5,6 @@ "on_panic": "Restart", "request_networking": true, "request_messaging": [ - "http_bindings:http_bindings:uqbar", - "encryptor:sys:uqbar", "http_server:sys:uqbar" ], "public": false diff --git a/modules/chess/pkg/metadata.json b/modules/chess/pkg/metadata.json index f833a906..59360b8e 100644 --- a/modules/chess/pkg/metadata.json +++ b/modules/chess/pkg/metadata.json @@ -1,5 +1,5 @@ { "package": "chess", "publisher": "uqbar", - "version": [0, 1, 0] + "version": [0, 2, 0] } diff --git a/modules/chess/src/lib.rs b/modules/chess/src/lib.rs index b3795a95..1456d0be 100644 --- a/modules/chess/src/lib.rs +++ b/modules/chess/src/lib.rs @@ -1,15 +1,17 @@ +#![feature(let_chains)] use serde::{Deserialize, Serialize}; -use serde_json::json; use std::collections::HashMap; extern crate base64; extern crate pleco; use pleco::Board; use uqbar_process_lib::uqbar::process::standard as wit; use uqbar_process_lib::{ - get_payload, get_typed_state, grant_messaging, println, receive, set_state, Address, Message, - Payload, ProcessId, Request, Response, + get_payload, get_typed_state, grant_messaging, http, println, receive, set_state, Address, + Message, Payload, ProcessId, Request, Response, }; +mod utils; + wit_bindgen::generate!({ path: "../../wit", world: "process", @@ -21,7 +23,7 @@ wit_bindgen::generate!({ struct Component; #[derive(Clone, Debug)] -struct Game { +pub struct Game { pub id: String, // the node with whom we are playing pub turns: u64, pub board: Board, @@ -31,7 +33,7 @@ struct Game { } #[derive(Clone, Debug, Serialize, Deserialize)] -struct StoredGame { +pub struct StoredGame { pub id: String, // the node with whom we are playing pub turns: u64, pub board: String, @@ -41,132 +43,17 @@ struct StoredGame { } #[derive(Clone, Debug)] -struct ChessState { +pub struct ChessState { pub games: HashMap, // game is by opposing player id pub records: HashMap, // wins, losses, draws } #[derive(Clone, Debug, Serialize, Deserialize)] -struct StoredChessState { +pub struct StoredChessState { pub games: HashMap, // game is by opposing player id pub records: HashMap, // wins, losses, draws } -fn convert_game(game: Game) -> StoredGame { - StoredGame { - id: game.id, - turns: game.turns, - board: game.board.fen(), - white: game.white, - black: game.black, - ended: game.ended, - } -} - -fn convert_state(state: ChessState) -> StoredChessState { - StoredChessState { - games: state - .games - .iter() - .map(|(id, game)| (id.to_string(), convert_game(game.clone()))) - .collect(), - records: state.records.clone(), - } -} - -fn json_game(game: &Game) -> serde_json::Value { - serde_json::json!({ - "id": game.id, - "turns": game.turns, - "board": game.board.fen(), - "white": game.white, - "black": game.black, - "ended": game.ended, - }) -} - -fn send_http_response( - status: u16, - headers: HashMap, - payload_bytes: Vec, -) -> anyhow::Result<()> { - Response::new() - .ipc_bytes( - serde_json::json!({ - "status": status, - "headers": headers, - }) - .to_string() - .as_bytes() - .to_vec(), - ) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: payload_bytes, - }) - .send() -} - -fn send_ws_update(our: Address, game: Game) -> anyhow::Result<()> { - Request::new() - .target(Address::new(&our.node, "encryptor:sys:uqbar").unwrap())? - .ipc_bytes( - serde_json::json!({ - "EncryptAndForwardAction": { - "channel_id": our.process.to_string(), - "forward_to": { - "node": our.node.clone(), - "process": { - "process_name": "http_server", - "package_name": "sys", - "publisher_node": "uqbar" - } - }, // node, process - "json": Some(serde_json::json!({ // this is the JSON to forward - "WebSocketPush": { - "target": { - "node": our.node.clone(), - "id": "chess", // If the message passed in an ID then we could send to just that ID - } - } - })), - } - - }) - .to_string() - .as_bytes() - .to_vec(), - ) - .payload(Payload { - mime: Some("application/json".to_string()), - bytes: serde_json::json!({ - "kind": "game_update", - "data": json_game(&game), - }) - .to_string() - .as_bytes() - .to_vec(), - }) - .send() -} - -fn response_success() -> bool { - let Some(payload) = get_payload() else { - return false; - }; - - let Ok(status) = String::from_utf8(payload.bytes) else { - return false; - }; - - status == "success" -} - -fn save_chess_state(state: ChessState) { - let stored_state = convert_state(state); - set_state(&bincode::serialize(&stored_state).unwrap()); -} - const CHESS_PAGE: &str = include_str!("../pkg/chess.html"); const CHESS_JS: &str = include_str!("../pkg/index.js"); const CHESS_CSS: &str = include_str!("../pkg/index.css"); @@ -174,73 +61,61 @@ const CHESS_CSS: &str = include_str!("../pkg/index.css"); impl Guest for Component { fn init(our: String) { let our = Address::from_str(&our).unwrap(); - println!("chess: start"); + println!("{our}: start"); grant_messaging( &our, - &Vec::from([ProcessId::from_str("http_server:sys:uqbar").unwrap()]), + vec![ProcessId::new(Some("http_server"), "sys", "uqbar")], ); - for path in ["/", "/games"] { - Request::new() - .target(Address::new(&our.node, "http_server:sys:uqbar").unwrap()) - .unwrap() - .ipc_bytes( - serde_json::json!({ - "BindPath": { - "path": path, - "authenticated": true, - "local_only": false - } - }) - .to_string() - .as_bytes() - .to_vec(), - ) - .send(); - } + // serve static page at / + // dynamically handle requests to /games + http::bind_http_static_path( + "/", + true, + false, + Some("text/html".to_string()), + CHESS_PAGE + .replace("${node}", &our.node) + .replace("${process}", &our.process.to_string()) + // TODO serve these independently on paths.. + // also build utils for just serving a vfs dir + .replace("${js}", CHESS_JS) + .replace("${css}", CHESS_CSS) + .as_bytes() + .to_vec(), + ) + .unwrap(); + http::bind_http_path("/games", true, false).unwrap(); - let mut state: ChessState = - match get_typed_state(|bytes| Ok(bincode::deserialize::(bytes)?)) { - Some(state) => { - let mut games = HashMap::new(); - for (id, game) in state.games { - if let Ok(board) = Board::from_fen(&game.board) { - games.insert( - id, - Game { - id: game.id.clone(), - turns: game.turns, - board, - white: game.white.clone(), - black: game.black.clone(), - ended: game.ended, - }, - ); - } else { - games.insert( - id, - Game { - id: game.id.clone(), - turns: 0, - board: Board::start_pos(), - white: game.white.clone(), - black: game.black.clone(), - ended: game.ended, - }, - ); - } - } - ChessState { - games, - records: state.records, - } - } - None => ChessState { - games: HashMap::new(), - records: HashMap::new(), - }, - }; + let mut state: ChessState = match get_typed_state(|bytes| { + Ok(bincode::deserialize::(bytes)?) + }) { + Some(mut state) => ChessState { + games: state + .games + .iter_mut() + .map(|(id, game)| { + ( + id.clone(), + Game { + id: id.to_owned(), + turns: game.turns, + board: Board::from_fen(&game.board).unwrap_or(Board::start_pos()), + white: game.white.to_owned(), + black: game.black.to_owned(), + ended: game.ended, + }, + ) + }) + .collect(), + records: state.records, + }, + None => ChessState { + games: HashMap::new(), + records: HashMap::new(), + }, + }; loop { let Ok((source, message)) = receive() else { @@ -251,12 +126,9 @@ impl Guest for Component { println!("chess: got unexpected Response"); continue; }; - match handle_request(&our, &source, &request, &mut state) { - Ok(_) => {} - Err(e) => { - println!("chess: error handling request: {:?}", e); - } + Ok(()) => continue, + Err(e) => println!("chess: error handling request: {:?}", e), } } } @@ -268,569 +140,410 @@ fn handle_request( request: &wit::Request, state: &mut ChessState, ) -> anyhow::Result<()> { - let message_json: serde_json::Value = match serde_json::from_slice(&request.ipc) { - Ok(v) => v, - Err(_) => return Err(anyhow::anyhow!("chess: failed to parse ipc JSON, skipping")), - }; - - // print_to_terminal(1, &format!("chess: parsed ipc JSON: {:?}", message_json)); - if source.process == "chess:chess:uqbar" { - let action = message_json["action"].as_str().unwrap_or(""); - let game_id = source.node.clone(); - - match action { - "new_game" => { - // make a new game with source.node if the current game has ended - if let Some(game) = state.games.get(&game_id) { - if !game.ended { - return Response::new() - .ipc_bytes(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "conflict".as_bytes().to_vec(), - }) - .send(); - } - } - let game = Game { - id: game_id.clone(), - turns: 0, - board: Board::start_pos(), - white: message_json["white"] - .as_str() - .unwrap_or(game_id.as_str()) - .to_string(), - black: message_json["black"] - .as_str() - .unwrap_or(our.node.as_str()) - .to_string(), - ended: false, - }; - state.games.insert(game_id.clone(), game.clone()); - - send_ws_update(our.clone(), game.clone()); - - save_chess_state(state.clone()); - - Response::new() - .ipc_bytes(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "success".as_bytes().to_vec(), - }) - .send() - } - "make_move" => { - // check the move and then update if correct and send WS update - let Some(game) = state.games.get_mut(&game_id) else { - return Response::new() - .ipc_bytes(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "not found".as_bytes().to_vec(), - }) - .send(); - }; - let valid_move = game - .board - .apply_uci_move(message_json["move"].as_str().unwrap_or("")); - if valid_move { - game.turns += 1; - let checkmate = game.board.checkmate(); - let draw = game.board.stalemate(); - - if checkmate || draw { - game.ended = true; - let winner = if checkmate { - if game.turns % 2 == 1 { - game.white.clone() - } else { - game.black.clone() - } - } else { - "".to_string() - }; - - // update the records - if draw { - if let Some(record) = state.records.get_mut(&game.id) { - record.2 += 1; - } else { - state.records.insert(game.id.clone(), (0, 0, 1)); - } - } else { - if let Some(record) = state.records.get_mut(&game.id) { - if winner == our.node { - record.0 += 1; - } else { - record.1 += 1; - } - } else { - if winner == our.node { - state.records.insert(game.id.clone(), (1, 0, 0)); - } else { - state.records.insert(game.id.clone(), (0, 1, 0)); - } - } - } - } - - send_ws_update(our.clone(), game.clone()); - save_chess_state(state.clone()); - - Response::new() - .ipc_bytes(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "success".as_bytes().to_vec(), - }) - .send() - } else { - Response::new() - .ipc_bytes(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "invalid move".as_bytes().to_vec(), - }) - .send() - } - } - "end_game" => { - // end the game and send WS update, update the standings - let Some(game) = state.games.get_mut(&game_id) else { - return Response::new() - .ipc_bytes(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "not found".as_bytes().to_vec(), - }) - .send(); - }; - - game.ended = true; - - if let Some(record) = state.records.get_mut(&game.id) { - record.0 += 1; - } else { - state.records.insert(game.id.clone(), (1, 0, 0)); - } - - send_ws_update(our.clone(), game.clone()); - save_chess_state(state.clone()); - - Response::new() - .ipc_bytes(vec![]) - .payload(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: "success".as_bytes().to_vec(), - }) - .send() - } - _ => return Err(anyhow::anyhow!("chess: got unexpected action")), - } + let message_json = serde_json::from_slice::(&request.ipc)?; + handle_chess_request(our, source, message_json, state) } else if source.process.to_string() == "http_server:sys:uqbar" { - let path = message_json["path"].as_str().unwrap_or(""); - let method = message_json["method"].as_str().unwrap_or(""); - - let mut default_headers = HashMap::new(); - default_headers.insert("Content-Type".to_string(), "text/html".to_string()); - // Handle incoming http - match path { - "/" => { - return send_http_response( - 200, - default_headers.clone(), - CHESS_PAGE - .replace("${node}", &our.node) - .replace("${process}", &our.process.to_string()) - .replace("${js}", CHESS_JS) - .replace("${css}", CHESS_CSS) - .to_string() - .as_bytes() - .to_vec(), - ); - } - "/games" => { - match method { - "GET" => { - return send_http_response( - 200, - { - let mut headers = default_headers.clone(); - headers.insert( - "Content-Type".to_string(), - "application/json".to_string(), - ); - headers - }, - { - let mut json_games: HashMap = - HashMap::new(); - for (id, game) in &state.games { - json_games.insert(id.to_string(), json_game(&game)); - } - json!(json_games).to_string().as_bytes().to_vec() - }, - ); - } - "POST" => { - // create a new game - if let Some(payload) = get_payload() { - if let Ok(payload_json) = - serde_json::from_slice::(&payload.bytes) - { - let game_id = - String::from(payload_json["id"].as_str().unwrap_or("")); - if game_id == "" { - return send_http_response( - 400, - default_headers.clone(), - "Bad Request".to_string().as_bytes().to_vec(), - ); - } - - if let Some(game) = state.games.get(&game_id) { - if !game.ended { - return send_http_response( - 409, - default_headers.clone(), - "Conflict".to_string().as_bytes().to_vec(), - ); - } - } - - let white = payload_json["white"] - .as_str() - .unwrap_or(our.node.as_str()) - .to_string(); - let black = payload_json["black"] - .as_str() - .unwrap_or(game_id.as_str()) - .to_string(); - - let response = Request::new() - .target(Address::new(&game_id, "chess:chess:uqbar")?)? - .ipc_bytes( - serde_json::json!({ - "action": "new_game", - "white": white.clone(), - "black": black.clone(), - }) - .to_string() - .as_bytes() - .to_vec(), - ) - .send_and_await_response(30)?; - - match response { - Ok(_response) => { - if !response_success() { - return send_http_response( - 503, - default_headers.clone(), - "Service Unavailable" - .to_string() - .as_bytes() - .to_vec(), - ); - } - // create a new game - let game = Game { - id: game_id.clone(), - turns: 0, - board: Board::start_pos(), - white: white.clone(), - black: black.clone(), - ended: false, - }; - state.games.insert(game_id.clone(), game.clone()); - - save_chess_state(state.clone()); - - return send_http_response( - 200, - { - let mut headers = default_headers.clone(); - headers.insert( - "Content-Type".to_string(), - "application/json".to_string(), - ); - headers - }, - json_game(&game).to_string().as_bytes().to_vec(), - ); - } - Err(_) => { - return send_http_response( - 503, - default_headers.clone(), - "Service Unavailable".to_string().as_bytes().to_vec(), - ) - } - } - } - } - return send_http_response( - 400, - default_headers.clone(), - "Bad Request".to_string().as_bytes().to_vec(), - ); - } - "PUT" => { - // make a move - if let Some(payload) = get_payload() { - if let Ok(payload_json) = - serde_json::from_slice::(&payload.bytes) - { - let game_id = - String::from(payload_json["id"].as_str().unwrap_or("")); - - if game_id == "" { - return send_http_response( - 400, - default_headers.clone(), - "No game ID".to_string().as_bytes().to_vec(), - ); - } - - if let Some(game) = state.games.get_mut(&game_id) { - if (game.turns % 2 == 0 && game.white != our.node) - || (game.turns % 2 == 1 && game.black != our.node) - { - return send_http_response( - 403, - default_headers.clone(), - "Forbidden".to_string().as_bytes().to_vec(), - ); - } else if game.ended { - return send_http_response( - 409, - default_headers.clone(), - "Conflict".to_string().as_bytes().to_vec(), - ); - } - - let move_str = payload_json["move"].as_str().unwrap_or(""); - let valid_move = game.board.apply_uci_move(move_str); - if valid_move { - // send the move to the other player - // check if the game is over - // if so, update the records - let response = Request::new() - .target(Address::new(&game_id, "chess:chess:uqbar")?)? - .ipc_bytes( - serde_json::json!({ - "action": "make_move", - "move": move_str, - }) - .to_string() - .as_bytes() - .to_vec(), - ) - .send_and_await_response(30)?; - - match response { - Ok(_response) => { - if !response_success() { - return send_http_response( - 503, - default_headers.clone(), - "Service Unavailable" - .to_string() - .as_bytes() - .to_vec(), - ); - } - // update the game - game.turns += 1; - let checkmate = game.board.checkmate(); - let draw = game.board.stalemate(); - - if checkmate || draw { - game.ended = true; - let winner = if checkmate { - if game.turns % 2 == 1 { - game.white.clone() - } else { - game.black.clone() - } - } else { - "".to_string() - }; - - // update the records - if draw { - if let Some(record) = - state.records.get_mut(&game.id) - { - record.2 += 1; - } else { - state - .records - .insert(game.id.clone(), (0, 0, 1)); - } - } else { - if let Some(record) = - state.records.get_mut(&game.id) - { - if winner == our.node { - record.0 += 1; - } else { - record.1 += 1; - } - } else { - if winner == our.node { - state.records.insert( - game.id.clone(), - (1, 0, 0), - ); - } else { - state.records.insert( - game.id.clone(), - (0, 1, 0), - ); - } - } - } - } - - let game = game.clone(); - save_chess_state(state.clone()); - // return the game - return send_http_response( - 200, - { - let mut headers = default_headers.clone(); - headers.insert( - "Content-Type".to_string(), - "application/json".to_string(), - ); - headers - }, - json_game(&game) - .to_string() - .as_bytes() - .to_vec(), - ); - } - Err(_) => { - return send_http_response( - 503, - default_headers.clone(), - "Service Unavailable" - .to_string() - .as_bytes() - .to_vec(), - ) - } - } - } - } - } - } - - println!("chess: never got a response"); - return send_http_response( - 400, - default_headers.clone(), - "Bad Request".to_string().as_bytes().to_vec(), - ); - } - "DELETE" => { - let game_id = message_json["query_params"]["id"] - .as_str() - .unwrap_or("") - .to_string(); - if game_id == "" { - return send_http_response( - 400, - default_headers.clone(), - "Bad Request".to_string().as_bytes().to_vec(), - ); - } else { - let Some(game) = state.games.get_mut(&game_id) else { - return send_http_response( - 400, - default_headers.clone(), - "Bad Request".to_string().as_bytes().to_vec(), - ); - }; - let response = Request::new() - .target(Address::new(&game_id, "chess:chess:uqbar")?)? - .ipc_bytes( - serde_json::json!({ - "action": "end_game", - }) - .to_string() - .as_bytes() - .to_vec(), - ) - .send_and_await_response(30)?; - - match response { - Ok(_response) => { - if !response_success() { - return send_http_response( - 503, - default_headers.clone(), - "Service Unavailable".to_string().as_bytes().to_vec(), - ); - } - - game.ended = true; - - if let Some(record) = state.records.get_mut(&game.id) { - record.1 += 1; - } else { - state.records.insert(game.id.clone(), (0, 1, 0)); - } - - let game = game.clone(); - save_chess_state(state.clone()); - - // return the game - return send_http_response( - 200, - { - let mut headers = default_headers.clone(); - headers.insert( - "Content-Type".to_string(), - "application/json".to_string(), - ); - headers - }, - json_game(&game).to_string().as_bytes().to_vec(), - ); - } - Err(_) => { - return send_http_response( - 503, - default_headers.clone(), - "Service Unavailable".to_string().as_bytes().to_vec(), - ); - } - } - } - } - _ => { - return send_http_response( - 404, - default_headers.clone(), - "Not Found".to_string().as_bytes().to_vec(), - ) - } - } - } - _ => { - return send_http_response( - 404, - default_headers.clone(), - "Not Found".to_string().as_bytes().to_vec(), - ) - } - } + let http_request = serde_json::from_slice::(&request.ipc)?; + handle_http_request(our, http_request, state) } else { return Err(anyhow::anyhow!("chess: got request from unexpected source")); } } + +fn handle_chess_request( + our: &Address, + source: &Address, + message_json: serde_json::Value, + state: &mut ChessState, +) -> anyhow::Result<()> { + let action = message_json["action"].as_str().unwrap_or(""); + let game_id = &source.node; + match action { + "new_game" => { + // make a new game with source.node if the current game has ended + if let Some(game) = state.games.get(game_id) { + if !game.ended { + return Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "conflict".as_bytes().to_vec(), + }) + .send(); + } + } + let game = Game { + id: game_id.to_string(), + turns: 0, + board: Board::start_pos(), + white: message_json["white"] + .as_str() + .unwrap_or(game_id) + .to_string(), + black: message_json["black"] + .as_str() + .unwrap_or(&our.node) + .to_string(), + ended: false, + }; + state.games.insert(game_id.to_string(), game.clone()); + + utils::send_ws_update(&our, &game)?; + utils::save_chess_state(&state); + + Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "success".as_bytes().to_vec(), + }) + .send() + } + "make_move" => { + // check the move and then update if correct and send WS update + let Some(game) = state.games.get_mut(game_id) else { + return Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "not found".as_bytes().to_vec(), + }) + .send(); + }; + let valid_move = game + .board + .apply_uci_move(message_json["move"].as_str().unwrap_or("")); + if valid_move { + game.turns += 1; + let checkmate = game.board.checkmate(); + let draw = game.board.stalemate(); + + if checkmate || draw { + game.ended = true; + let winner = if checkmate { + if game.turns % 2 == 1 { + game.white.clone() + } else { + game.black.clone() + } + } else { + "".to_string() + }; + + // update the records + if draw { + if let Some(record) = state.records.get_mut(&game.id) { + record.2 += 1; + } else { + state.records.insert(game.id.clone(), (0, 0, 1)); + } + } else { + if let Some(record) = state.records.get_mut(&game.id) { + if winner == our.node { + record.0 += 1; + } else { + record.1 += 1; + } + } else { + if winner == our.node { + state.records.insert(game.id.clone(), (1, 0, 0)); + } else { + state.records.insert(game.id.clone(), (0, 1, 0)); + } + } + } + } + + utils::send_ws_update(&our, &game)?; + utils::save_chess_state(&state); + + Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "success".as_bytes().to_vec(), + }) + .send() + } else { + Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "invalid move".as_bytes().to_vec(), + }) + .send() + } + } + "end_game" => { + // end the game and send WS update, update the standings + let Some(game) = state.games.get_mut(game_id) else { + return Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "not found".as_bytes().to_vec(), + }) + .send(); + }; + + game.ended = true; + + if let Some(record) = state.records.get_mut(&game.id) { + record.0 += 1; + } else { + state.records.insert(game.id.clone(), (1, 0, 0)); + } + + utils::send_ws_update(&our, &game)?; + utils::save_chess_state(&state); + + Response::new() + .ipc(vec![]) + .payload(Payload { + mime: Some("application/octet-stream".to_string()), + bytes: "success".as_bytes().to_vec(), + }) + .send() + } + _ => return Err(anyhow::anyhow!("chess: got unexpected action")), + } +} + +fn handle_http_request( + our: &Address, + http_request: http::IncomingHttpRequest, + state: &mut ChessState, +) -> anyhow::Result<()> { + if http_request.path()? != "/games" { + return http::send_response( + http::StatusCode::NOT_FOUND, + None, + "Not Found".to_string().as_bytes().to_vec(), + ); + } + match http_request.method.as_str() { + "GET" => http::send_response( + http::StatusCode::OK, + Some(HashMap::from([( + String::from("Content-Type"), + String::from("application/json"), + )])), + serde_json::to_vec(&serde_json::json!(state + .games + .iter() + .map(|(id, game)| (id.to_string(), utils::json_game(game))) + .collect::>()))?, + ), + "POST" => { + // create a new game + let Some(payload) = get_payload() else { + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + }; + let payload_json = serde_json::from_slice::(&payload.bytes)?; + let Some(game_id) = payload_json["id"].as_str() else { + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + }; + if let Some(game) = state.games.get(game_id) + && !game.ended + { + return http::send_response(http::StatusCode::CONFLICT, None, vec![]); + }; + + let player_white = payload_json["white"] + .as_str() + .unwrap_or(our.node.as_str()) + .to_string(); + let player_black = payload_json["black"] + .as_str() + .unwrap_or(game_id) + .to_string(); + + // send the other player a new game request + let response = Request::new() + .target((game_id, "chess", "chess", "uqbar")) + .ipc(serde_json::to_vec(&serde_json::json!({ + "action": "new_game", + "white": player_white.clone(), + "black": player_black.clone(), + }))?) + .send_and_await_response(30)?; + // if they accept, create a new game + // otherwise, should surface error to FE... + let Ok((_source, Message::Response((resp, _context)))) = response else { + return http::send_response( + http::StatusCode::SERVICE_UNAVAILABLE, + None, + "Service Unavailable".to_string().as_bytes().to_vec(), + ); + }; + if resp.ipc != "success".as_bytes() { + return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]); + } + // create a new game + let game = Game { + id: game_id.to_string(), + turns: 0, + board: Board::start_pos(), + white: player_white, + black: player_black, + ended: false, + }; + let body = serde_json::to_vec(&utils::json_game(&game))?; + state.games.insert(game_id.to_string(), game); + utils::save_chess_state(&state); + http::send_response( + http::StatusCode::OK, + Some(HashMap::from([( + String::from("Content-Type"), + String::from("application/json"), + )])), + body, + ) + } + "PUT" => { + // make a move + let Some(payload) = get_payload() else { + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + }; + let payload_json = serde_json::from_slice::(&payload.bytes)?; + let Some(game_id) = payload_json["id"].as_str() else { + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + }; + let Some(game) = state.games.get_mut(game_id) else { + return http::send_response(http::StatusCode::NOT_FOUND, None, vec![]); + }; + if (game.turns % 2 == 0 && game.white != our.node) + || (game.turns % 2 == 1 && game.black != our.node) + { + return http::send_response(http::StatusCode::FORBIDDEN, None, vec![]); + } else if game.ended { + return http::send_response(http::StatusCode::CONFLICT, None, vec![]); + } + let move_str = payload_json["move"].as_str().unwrap_or(""); + if !game.board.apply_uci_move(move_str) { + // TODO surface illegal move to player or something here + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + } + // send the move to the other player + // check if the game is over + // if so, update the records + let response = Request::new() + .target((game_id, "chess", "chess", "uqbar")) + .ipc(serde_json::to_vec(&serde_json::json!({ + "action": "make_move", + "move": move_str, + }))?) + .send_and_await_response(30)?; + let Ok((_source, Message::Response((resp, _context)))) = response else { + // TODO surface error to player, let them know other player is + // offline or whatever they respond here was invalid + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + }; + if resp.ipc != "success".as_bytes() { + return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]); + } + // update the game + game.turns += 1; + let checkmate = game.board.checkmate(); + let draw = game.board.stalemate(); + + if checkmate || draw { + game.ended = true; + let winner = if checkmate { + if game.turns % 2 == 1 { + &game.white + } else { + &game.black + } + } else { + "" + }; + // update the records + if draw { + if let Some(record) = state.records.get_mut(&game.id) { + record.2 += 1; + } else { + state.records.insert(game.id.clone(), (0, 0, 1)); + } + } else { + if let Some(record) = state.records.get_mut(&game.id) { + if winner == our.node { + record.0 += 1; + } else { + record.1 += 1; + } + } else { + if winner == our.node { + state.records.insert(game.id.clone(), (1, 0, 0)); + } else { + state.records.insert(game.id.clone(), (0, 1, 0)); + } + } + } + } + // game is not over, update state and return to FE + let body = serde_json::to_vec(&utils::json_game(&game))?; + utils::save_chess_state(&state); + // return the game + http::send_response( + http::StatusCode::OK, + Some(HashMap::from([( + String::from("Content-Type"), + String::from("application/json"), + )])), + body, + ) + } + "DELETE" => { + // "end the game"? + let query_params = http_request.query_params()?; + let Some(game_id) = query_params.get("id") else { + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + }; + let Some(game) = state.games.get_mut(game_id) else { + return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); + }; + // send the other player an end game request + let response = Request::new() + .target((game_id, "chess", "chess", "uqbar")) + .ipc(serde_json::to_vec(&serde_json::json!({ + "action": "end_game", + }))?) + .send_and_await_response(30)?; + let Ok((_source, Message::Response((resp, _context)))) = response else { + // TODO surface error to player, let them know other player is + // offline or whatever they respond here was invalid + return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]); + }; + if resp.ipc != "success".as_bytes() { + return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]); + } + + game.ended = true; + if let Some(record) = state.records.get_mut(&game.id) { + record.1 += 1; + } else { + state.records.insert(game.id.clone(), (0, 1, 0)); + } + // return the game + let body = serde_json::to_vec(&utils::json_game(&game))?; + utils::save_chess_state(&state); + + http::send_response( + http::StatusCode::OK, + Some(HashMap::from([( + String::from("Content-Type"), + String::from("application/json"), + )])), + body, + ) + } + _ => Response::new() + .ipc(serde_json::to_vec(&http::HttpResponse { + status: 405, + headers: HashMap::new(), + })?) + .send(), + } +} diff --git a/modules/chess/src/utils.rs b/modules/chess/src/utils.rs new file mode 100644 index 00000000..4416af91 --- /dev/null +++ b/modules/chess/src/utils.rs @@ -0,0 +1,82 @@ +use crate::*; + +pub fn save_chess_state(state: &ChessState) { + let stored_state = convert_state(&state); + set_state(&bincode::serialize(&stored_state).unwrap()); +} + +fn convert_game(game: Game) -> StoredGame { + StoredGame { + id: game.id, + turns: game.turns, + board: game.board.fen(), + white: game.white, + black: game.black, + ended: game.ended, + } +} + +fn convert_state(state: &ChessState) -> StoredChessState { + StoredChessState { + games: state + .games + .iter() + .map(|(id, game)| (id.to_string(), convert_game(game.clone()))) + .collect(), + records: state.records.clone(), + } +} + +pub fn json_game(game: &Game) -> serde_json::Value { + serde_json::json!({ + "id": game.id, + "turns": game.turns, + "board": game.board.fen(), + "white": game.white, + "black": game.black, + "ended": game.ended, + }) +} + +pub fn send_ws_update(our: &Address, game: &Game) -> anyhow::Result<()> { + Request::new() + .target((&our.node, "http_server", "sys", "uqbar")) + .ipc( + serde_json::json!({ + "EncryptAndForward": { + "channel_id": our.process.to_string(), + "forward_to": { + "node": our.node.clone(), + "process": { + "process_name": "http_server", + "package_name": "sys", + "publisher_node": "uqbar" + } + }, // node, process + "json": Some(serde_json::json!({ // this is the JSON to forward + "WebSocketPush": { + "target": { + "node": our.node.clone(), + "id": "chess", // If the message passed in an ID then we could send to just that ID + } + } + })), + } + + }) + .to_string() + .as_bytes() + .to_vec(), + ) + .payload(Payload { + mime: Some("application/json".to_string()), + bytes: serde_json::json!({ + "kind": "game_update", + "data": json_game(game), + }) + .to_string() + .as_bytes() + .to_vec(), + }) + .send() +} diff --git a/modules/homepage/Cargo.lock b/modules/homepage/Cargo.lock index 53d2d940..110b5c94 100644 --- a/modules/homepage/Cargo.lock +++ b/modules/homepage/Cargo.lock @@ -237,6 +237,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -257,13 +277,15 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "uqbar_process_lib" -version = "0.2.0" -source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=e53c124#e53c124ec95ef99c06d201d4d08dada8ec691d29" +version = "0.3.0" +source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=abbe406#abbe4060e3fa8cdf3e0fa11ba7362f5860c5fce3" dependencies = [ "anyhow", "bincode", "rand", "serde", + "serde_json", + "thiserror", "wit-bindgen", ] diff --git a/modules/homepage/Cargo.toml b/modules/homepage/Cargo.toml index a9e2c1e5..61cb3d32 100644 --- a/modules/homepage/Cargo.toml +++ b/modules/homepage/Cargo.toml @@ -15,7 +15,7 @@ anyhow = "1.0" bincode = "1.3.3" serde = {version = "1.0", features = ["derive"] } serde_json = "1.0" -uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" } +uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "abbe406" } wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "5390bab780733f1660d14c254ec985df2816bf1d" } [lib] diff --git a/modules/homepage/src/home.html b/modules/homepage/src/home.html index c73fb4ea..3c47dc88 100644 --- a/modules/homepage/src/home.html +++ b/modules/homepage/src/home.html @@ -196,7 +196,7 @@

Apps:

- Chess + Chess [NOT WORKING] HTTP Proxy diff --git a/modules/homepage/src/lib.rs b/modules/homepage/src/lib.rs index 20f25c5a..45d9b1cb 100644 --- a/modules/homepage/src/lib.rs +++ b/modules/homepage/src/lib.rs @@ -1,7 +1,7 @@ -use serde_json::json; +#![feature(let_chains)] use uqbar_process_lib::{ - get_payload, grant_messaging, println, receive, Address, Message, Payload, ProcessId, Request, - Response, + grant_messaging, http::bind_http_static_path, http::HttpServerError, println, receive, Address, + Message, ProcessId, Response, }; wit_bindgen::generate!({ @@ -16,20 +16,10 @@ struct Component; const HOME_PAGE: &str = include_str!("home.html"); -fn serialize_json_message(message: &serde_json::Value) -> anyhow::Result> { - Ok(serde_json::to_vec(message)?) -} - impl Guest for Component { fn init(our: String) { let our = Address::from_str(&our).unwrap(); - println!("homepage: start"); - - grant_messaging( - &our, - &Vec::from([ProcessId::from_str("http_server:sys:uqbar").unwrap()]), - ); - + grant_messaging(&our, vec![ProcessId::new(Some("http_server"), "sys", "uqbar")]); match main(our) { Ok(_) => {} Err(e) => { @@ -40,141 +30,34 @@ impl Guest for Component { } fn main(our: Address) -> anyhow::Result<()> { - // bind to root path on http_server - Request::new() - .target(Address::new(&our.node, "http_server:sys:uqbar")?)? - .ipc( - &json!({ - "BindPath": { - "path": "/", - "authenticated": true, - "local_only": false - } - }), - serialize_json_message, - )? - .send()?; + // bind to root path on http_server (we have special dispensation to do so!) + bind_http_static_path( + "/", + true, + false, + Some("text/html".to_string()), + HOME_PAGE + .replace("${our}", &our.node) + .to_string() + .as_bytes() + .to_vec(), + )?; loop { - let Ok((_source, message)) = receive() else { - println!("homepage: got network error"); + let Ok((ref source, ref message)) = receive() else { + println!("homepage: got network error??"); continue; }; - let Message::Request(request) = message else { - println!("homepage: got unexpected message: {:?}", message); - continue; - }; - - let message_json: serde_json::Value = match serde_json::from_slice(&request.ipc) { - Ok(v) => v, - Err(_) => { - println!("homepage: failed to parse ipc JSON, skipping"); - continue; + if let Message::Response((ref msg, _)) = message + && source.process == "http_server:sys:uqbar" + { + match serde_json::from_slice::>(&msg.ipc) { + Ok(Ok(())) => continue, + Ok(Err(e)) => println!("homepage: got error from http_server: {e}"), + Err(_e) => println!("homepage: got malformed message from http_server!"), } - }; - - if message_json["path"] == "/" && message_json["method"] == "GET" { - Response::new() - .ipc( - &json!({ - "action": "response", - "status": 200, - "headers": { - "Content-Type": "text/html", - }, - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("text/html".to_string()), - bytes: HOME_PAGE - .replace("${our}", &our.node) - .to_string() - .as_bytes() - .to_vec(), - }) - .send()?; - } else if message_json["path"].is_string() { - Response::new() - .ipc( - &json!({ - "action": "response", - "status": 404, - "headers": { - "Content-Type": "text/html", - }, - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("text/html".to_string()), - bytes: "Not Found".to_string().as_bytes().to_vec(), - }) - .send()?; - } else if message_json["hello"] == "world" { - Response::new() - .ipc( - &json!({ - "hello": "to you too" - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("application/json".to_string()), - bytes: serde_json::json!({ - "hello": "to you too" - }) - .to_string() - .as_bytes() - .to_vec(), - }) - .send()?; } else { - if let Some(payload) = get_payload() { - if let Ok(json) = serde_json::from_slice::(&payload.bytes) { - // println!("JSON: {}", json); - if json["message"] == "ping" { - // WebSocket pushes are sent as requests - Request::new() - .target(Address::new(&our.node, "encryptor:sys:uqbar")?)? - .ipc( - &json!({ - "EncryptAndForwardAction": { - "channel_id": "homepage", - "forward_to": { - "node": our.node.clone(), - "process": { - "process_name": "http_server", - "package_name": "sys", - "publisher_node": "uqbar" - } - }, // node, process - "json": Some(json!({ // this is the JSON to forward - "WebSocketPush": { - "target": { - "node": our.node.clone(), - "id": "homepage", // If the message passed in an ID then we could send to just that ID - } - } - })), - } - - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("application/json".to_string()), - bytes: serde_json::json!({ - "pong": true - }) - .to_string() - .as_bytes() - .to_vec(), - }) - .send()?; - } - } - } + println!("homepage: got message from {source:?}: {message:?}"); } } } diff --git a/modules/http_proxy/Cargo.lock b/modules/http_proxy/Cargo.lock deleted file mode 100644 index 1129e591..00000000 --- a/modules/http_proxy/Cargo.lock +++ /dev/null @@ -1,390 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "anyhow" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "getrandom" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "hashbrown" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "http_proxy" -version = "0.2.0" -dependencies = [ - "anyhow", - "bincode", - "serde", - "serde_json", - "uqbar_process_lib", - "wit-bindgen", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "indexmap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" -dependencies = [ - "equivalent", - "hashbrown", - "serde", -] - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "libc" -version = "0.2.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "semver" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" - -[[package]] -name = "serde" -version = "1.0.191" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a834c4821019838224821468552240d4d95d14e751986442c816572d39a080c9" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.191" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fa52d5646bce91b680189fe5b1c049d2ea38dabb4e2e7c8d00ca12cfbfbcfd" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "smallvec" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" - -[[package]] -name = "spdx" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b19b32ed6d899ab23174302ff105c1577e45a06b08d4fe0a9dd13ce804bbbf71" -dependencies = [ - "smallvec", -] - -[[package]] -name = "syn" -version = "2.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "uqbar_process_lib" -version = "0.2.0" -source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=e53c124#e53c124ec95ef99c06d201d4d08dada8ec691d29" -dependencies = [ - "anyhow", - "bincode", - "rand", - "serde", - "wit-bindgen", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-encoder" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822b645bf4f2446b949776ffca47e2af60b167209ffb70814ef8779d299cd421" -dependencies = [ - "leb128", -] - -[[package]] -name = "wasm-metadata" -version = "0.10.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2167ce53b2faa16a92c6cafd4942cff16c9a4fa0c5a5a0a41131ee4e49fc055f" -dependencies = [ - "anyhow", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" -dependencies = [ - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.13.1" -source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=5390bab780733f1660d14c254ec985df2816bf1d#5390bab780733f1660d14c254ec985df2816bf1d" -dependencies = [ - "bitflags", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.13.1" -source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=5390bab780733f1660d14c254ec985df2816bf1d#5390bab780733f1660d14c254ec985df2816bf1d" -dependencies = [ - "anyhow", - "wit-component", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.13.2" -source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=5390bab780733f1660d14c254ec985df2816bf1d#5390bab780733f1660d14c254ec985df2816bf1d" -dependencies = [ - "anyhow", - "heck", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.13.1" -source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=5390bab780733f1660d14c254ec985df2816bf1d#5390bab780733f1660d14c254ec985df2816bf1d" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", - "wit-component", -] - -[[package]] -name = "wit-component" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480cc1a078b305c1b8510f7c455c76cbd008ee49935f3a6c5fd5e937d8d95b1e" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43771ee863a16ec4ecf9da0fc65c3bbd4a1235c8e3da5f094b562894843dfa76" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", -] diff --git a/modules/http_proxy/Cargo.toml b/modules/http_proxy/Cargo.toml deleted file mode 100644 index c1a8e396..00000000 --- a/modules/http_proxy/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "http_proxy" -version = "0.2.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[profile.release] -panic = "abort" -opt-level = "s" -lto = true - -[dependencies] -anyhow = "1.0" -bincode = "1.3.3" -serde = {version = "1.0", features = ["derive"] } -serde_json = "1.0" -uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" } -wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "5390bab780733f1660d14c254ec985df2816bf1d" } - -[lib] -crate-type = ["cdylib"] - -[package.metadata.component] -package = "uqbar:process" diff --git a/modules/http_proxy/pkg/manifest.json b/modules/http_proxy/pkg/manifest.json deleted file mode 100644 index 46bb2e76..00000000 --- a/modules/http_proxy/pkg/manifest.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "process_name": "http_proxy", - "process_wasm_path": "/http_proxy.wasm", - "on_panic": "Restart", - "request_networking": false, - "request_messaging": [ - "http_bindings:http_bindings:uqbar", - "encryptor:sys:uqbar", - "http_server:sys:uqbar" - ], - "public": false - } -] diff --git a/modules/http_proxy/pkg/metadata.json b/modules/http_proxy/pkg/metadata.json deleted file mode 100644 index 8fc0ad14..00000000 --- a/modules/http_proxy/pkg/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "package": "http_proxy", - "publisher": "uqbar", - "version": [0, 1, 0] -} diff --git a/modules/http_proxy/src/http_proxy.html b/modules/http_proxy/src/http_proxy.html deleted file mode 100644 index 7d1f496f..00000000 --- a/modules/http_proxy/src/http_proxy.html +++ /dev/null @@ -1,308 +0,0 @@ - - - - ${our} - HTTP Proxy - - - - - - - - -
-
- -

HTTP Proxy Manager

-
- -
-
- -
- - -
-
-
-

Currently Proxying For:

-
    -
    -
    -
    - - - diff --git a/modules/http_proxy/src/lib.rs b/modules/http_proxy/src/lib.rs deleted file mode 100644 index fd8dde54..00000000 --- a/modules/http_proxy/src/lib.rs +++ /dev/null @@ -1,300 +0,0 @@ -use serde_json::json; -use std::collections::HashMap; -use uqbar_process_lib::{ - get_payload, grant_messaging, println, receive, Address, Message, Payload, ProcessId, Request, - Response, -}; - -wit_bindgen::generate!({ - path: "../../wit", - world: "process", - exports: { - world: Component, - }, -}); - -struct Component; -impl Guest for Component { - fn init(our: String) { - let our = Address::from_str(&our).unwrap(); - - grant_messaging( - &our, - &Vec::from([ProcessId::from_str("http_server:sys:uqbar").unwrap()]), - ); - - match main(our) { - Ok(_) => {} - Err(e) => { - println!("http_proxy: ended with error: {:?}", e); - } - } - } -} - -const PROXY_HOME_PAGE: &str = include_str!("http_proxy.html"); - -fn serialize_json_message(message: &serde_json::Value) -> anyhow::Result> { - Ok(serde_json::to_vec(message)?) -} - -fn send_http_response( - status: u16, - headers: HashMap, - payload_bytes: Vec, -) -> anyhow::Result<()> { - Response::new() - .ipc( - &json!({ - "status": status, - "headers": headers, - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("text/html".to_string()), - bytes: payload_bytes, - }) - .send()?; - Ok(()) -} - -fn send_not_found() -> anyhow::Result<()> { - send_http_response( - 404, - HashMap::new(), - "Not Found".to_string().as_bytes().to_vec(), - ) -} - -fn main(our: Address) -> anyhow::Result<()> { - let mut registrations: HashMap = HashMap::new(); - - // bind to all of our favorite paths - for path in ["/", "/static/*", "/list", "/register", "/serve/:username/*"] { - Request::new() - .target(Address::new(&our.node, "http_server:sys:uqbar")?)? - .ipc( - &json!({ - "BindPath": { - "path": path, - "authenticated": true, - "local_only": false - } - }), - serialize_json_message, - )? - .send()?; - } - - loop { - let Ok((_source, message)) = receive() else { - //print_to_terminal(0, "http_proxy: got network error"); - let mut headers = HashMap::new(); - headers.insert("Content-Type".to_string(), "text/html".to_string()); - send_http_response( - 503, - headers, - format!("

    Node Offline

    ").as_bytes().to_vec(), - )?; - continue; - }; - let Message::Request(request) = message else { - println!("http_proxy: got unexpected message"); - continue; - }; - - let message_json: serde_json::Value = match serde_json::from_slice(&request.ipc) { - Ok(v) => v, - Err(_) => { - //print_to_terminal(1, "http_proxy: failed to parse ipc JSON, skipping"); - continue; - } - }; - - //print_to_terminal( - // 1, - // format!("http_proxy: got request: {}", message_json).as_str(), - //); - - if message_json["path"] == "/" && message_json["method"] == "GET" { - Response::new() - .ipc( - &json!({ - "action": "response", - "status": 200, - "headers": { - "Content-Type": "text/html", - }, - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("text/html".to_string()), - bytes: PROXY_HOME_PAGE - .replace("${our}", &our.node) - .as_bytes() - .to_vec(), - }) - .send()?; - } else if message_json["path"] == "/list" && message_json["method"] == "GET" { - Response::new() - .ipc( - &json!({ - "action": "response", - "status": 200, - "headers": { - "Content-Type": "application/json", - }, - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("application/json".to_string()), - bytes: serde_json::json!({"registrations": registrations}) - .to_string() - .as_bytes() - .to_vec(), - }) - .send()?; - } else if message_json["path"] == "/register" && message_json["method"] == "POST" { - let mut status = 204; - - let Some(payload) = get_payload() else { - //print_to_terminal(1, "/register POST with no bytes"); - continue; - }; - - let body: serde_json::Value = match serde_json::from_slice(&payload.bytes) { - Ok(s) => s, - Err(e) => { - //print_to_terminal(1, format!("Bad body format: {}", e).as_str()); - continue; - } - }; - - let username = body["username"].as_str().unwrap_or(""); - - //print_to_terminal(1, format!("Register proxy for: {}", username).as_str()); - - if !username.is_empty() { - registrations.insert(username.to_string(), "foo".to_string()); - } else { - status = 400; - } - - Response::new() - .ipc( - &json!({ - "action": "response", - "status": 200, - "headers": { - "Content-Type": "text/html", - }, - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("text/html".to_string()), - bytes: (if status == 400 { - "Bad Request" - } else { - "Success" - }) - .to_string() - .as_bytes() - .to_vec(), - }) - .send()?; - } else if message_json["path"] == "/register" && message_json["method"] == "DELETE" { - //print_to_terminal(1, "HERE IN /register to delete something"); - let username = message_json["query_params"]["username"] - .as_str() - .unwrap_or(""); - - let mut status = 204; - - if !username.is_empty() { - registrations.remove(username); - } else { - status = 400; - } - - Response::new() - .ipc( - &json!({ - "action": "response", - "status": status, - "headers": { - "Content-Type": "text/html", - }, - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("text/html".to_string()), - bytes: (if status == 400 { - "Bad Request" - } else { - "Success" - }) - .to_string() - .as_bytes() - .to_vec(), - }) - .send()?; - } else if message_json["path"] == "/serve/:username/*" { - let username = message_json["url_params"]["username"] - .as_str() - .unwrap_or(""); - let raw_path = message_json["raw_path"].as_str().unwrap_or(""); - //print_to_terminal(1, format!("proxy for user: {}", username).as_str()); - - if username.is_empty() || raw_path.is_empty() { - send_not_found()?; - } else if !registrations.contains_key(username) { - Response::new() - .ipc( - &json!({ - "action": "response", - "status": 403, - "headers": { - "Content-Type": "text/html", - }, - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("text/html".to_string()), - bytes: "Not Authorized".to_string().as_bytes().to_vec(), - }) - .send()?; - } else { - let path_parts: Vec<&str> = raw_path.split('/').collect(); - let mut proxied_path = "/".to_string(); - - if let Some(pos) = path_parts.iter().position(|&x| x == "serve") { - proxied_path = format!("/{}", path_parts[pos + 2..].join("/")); - //print_to_terminal(1, format!("Path to proxy: {}", proxied_path).as_str()); - } - - Request::new() - .target(Address::new(&username, "http_server:sys:uqbar")?)? - .inherit(true) - .ipc( - &json!({ - "method": message_json["method"], - "path": proxied_path, - "headers": message_json["headers"], - "proxy_path": raw_path, - "query_params": message_json["query_params"], - }), - serialize_json_message, - )? - .send()?; - } - } else { - send_not_found()?; - } - } -} diff --git a/modules/qns_indexer/Cargo.lock b/modules/qns_indexer/Cargo.lock index 3d53fabb..2f73f46f 100644 --- a/modules/qns_indexer/Cargo.lock +++ b/modules/qns_indexer/Cargo.lock @@ -635,6 +635,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -670,13 +690,15 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "uqbar_process_lib" -version = "0.2.0" -source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=e53c124#e53c124ec95ef99c06d201d4d08dada8ec691d29" +version = "0.3.0" +source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=abbe406#abbe4060e3fa8cdf3e0fa11ba7362f5860c5fce3" dependencies = [ "anyhow", "bincode", "rand", "serde", + "serde_json", + "thiserror", "wit-bindgen", ] diff --git a/modules/qns_indexer/Cargo.toml b/modules/qns_indexer/Cargo.toml index e3ebb8ca..77615528 100644 --- a/modules/qns_indexer/Cargo.toml +++ b/modules/qns_indexer/Cargo.toml @@ -19,7 +19,7 @@ hex = "0.4.3" rmp-serde = "1.1.2" serde = {version = "1.0", features = ["derive"] } serde_json = "1.0" -uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" } +uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "abbe406" } wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "5390bab780733f1660d14c254ec985df2816bf1d" } [lib] diff --git a/modules/qns_indexer/src/lib.rs b/modules/qns_indexer/src/lib.rs index bf78d058..d7898982 100644 --- a/modules/qns_indexer/src/lib.rs +++ b/modules/qns_indexer/src/lib.rs @@ -6,8 +6,7 @@ use serde_json::json; use std::collections::HashMap; use std::string::FromUtf8Error; use uqbar_process_lib::{ - get_typed_state, receive, set_state, Address, Message, Payload, Request, - Response, + get_typed_state, http, receive, set_state, Address, Message, Payload, Request, Response, }; wit_bindgen::generate!({ @@ -67,6 +66,14 @@ pub struct QnsUpdate { pub routers: Vec, } +impl TryInto> for NetActions { + type Error = anyhow::Error; + + fn try_into(self) -> Result, Self::Error> { + Ok(rmp_serde::to_vec(&self)?) + } +} + sol! { event WsChanged( uint256 indexed node, @@ -103,14 +110,6 @@ fn subscribe_to_qns(from_block: u64) -> Vec { .to_vec() } -fn serialize_message(message: &NetActions) -> anyhow::Result> { - Ok(rmp_serde::to_vec(message)?) -} - -fn serialize_json_message(message: &serde_json::Value) -> anyhow::Result> { - Ok(serde_json::to_vec(message)?) -} - impl Guest for Component { fn init(our: String) { let our = Address::from_str(&our).unwrap(); @@ -143,32 +142,19 @@ impl Guest for Component { fn main(our: Address, mut state: State) -> anyhow::Result<()> { // shove all state into net::net Request::new() - .target(Address::new(&our.node, "net:sys:uqbar")?)? - .ipc( - &NetActions::QnsBatchUpdate(state.nodes.values().cloned().collect::>()), - serialize_message, - )? + .target((&our.node, "net", "sys", "uqbar")) + .try_ipc(NetActions::QnsBatchUpdate( + state.nodes.values().cloned().collect::>(), + ))? .send()?; Request::new() - .target(Address::new(&our.node, "eth_rpc:sys:uqbar")?)? - .ipc_bytes(subscribe_to_qns(state.block - 1)) + .target((&our.node, "eth_rpc", "sys", "uqbar")) + .ipc(subscribe_to_qns(state.block - 1)) .expects_response(5) .send()?; - Request::new() - .target(Address::new(&our.node, "http_server:sys:uqbar")?)? - .ipc( - &json!({ - "BindPath": { - "path": "/node/:name", - "authenticated": false, - "local_only": false - } - }), - serialize_json_message, - )? - .send()?; + http::bind_http_path("/node/:name", false, false)?; loop { let Ok((source, message)) = receive() else { @@ -188,14 +174,15 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> { if let Some(node) = state.nodes.get(name) { Response::new() .ipc( - &serde_json::json!({ - "status": 200, - "headers": { - "Content-Type": "application/json", - }, - }), - serialize_json_message, - )? + serde_json::to_vec(&http::HttpResponse { + status: 200, + headers: HashMap::from([( + "Content-Type".to_string(), + "application/json".to_string(), + )]), + }) + .unwrap(), + ) .payload(Payload { mime: Some("application/json".to_string()), bytes: serde_json::to_string(&node) @@ -211,18 +198,15 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> { } Response::new() .ipc( - &serde_json::json!({ - "status": 404, - "headers": { - "Content-Type": "application/json", - }, - }), - serialize_json_message, - )? - .payload(Payload { - mime: Some("application/json".to_string()), - bytes: "Not Found".to_string().as_bytes().to_vec(), - }) + serde_json::to_vec(&http::HttpResponse { + status: 404, + headers: HashMap::from([( + "Content-Type".to_string(), + "application/json".to_string(), + )]), + }) + .unwrap(), + ) .send()?; continue; } @@ -299,8 +283,8 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> { state.nodes.insert(name.clone(), update.clone()); Request::new() - .target(Address::new(&our.node, "net:sys:uqbar")?)? - .ipc(&NetActions::QnsUpdate(update.clone()), serialize_message)? + .target((&our.node, "net", "sys", "uqbar")) + .try_ipc(NetActions::QnsUpdate(update))? .send()?; } event => { @@ -309,7 +293,7 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> { } } } - set_state(&bincode::serialize(&state)?); + set_state(&bincode::serialize(&state)?); } } // helpers diff --git a/modules/terminal/pkg/manifest.json b/modules/terminal/pkg/manifest.json index 16400b67..18164233 100644 --- a/modules/terminal/pkg/manifest.json +++ b/modules/terminal/pkg/manifest.json @@ -5,7 +5,8 @@ "on_panic": "Restart", "request_networking": true, "request_messaging": [ - "net:sys:uqbar" + "net:sys:uqbar", + "http_client:sys:uqbar" ], "public": true } diff --git a/src/encryptor.rs b/src/encryptor.rs deleted file mode 100644 index 3f1bc69a..00000000 --- a/src/encryptor.rs +++ /dev/null @@ -1,429 +0,0 @@ -extern crate generic_array; -extern crate num_traits; -extern crate rand; - -use crate::types::*; -use aes_gcm::{ - aead::{Aead, KeyInit}, - Aes256Gcm, - Key, // Or `Aes128Gcm` - Nonce, -}; -use anyhow::Result; -use generic_array::GenericArray; -use rand::{thread_rng, Rng}; -use ring::signature::Ed25519KeyPair; -use rsa::{BigUint, Oaep, RsaPublicKey}; - -use std::collections::HashMap; -use std::sync::Arc; - -use crate::encryptor::num_traits::Num; - -fn encrypt_data(secret_key_bytes: [u8; 32], data: Vec) -> Vec { - let key = Key::::from_slice(&secret_key_bytes); - let cipher = Aes256Gcm::new(key); - - let mut nonce_bytes: [u8; 12] = [0; 12]; - thread_rng().fill(&mut nonce_bytes); - let nonce = Nonce::from_slice(&nonce_bytes); - - let ciphertext = cipher - .encrypt(nonce, data.as_ref()) - .expect("encryption failure!"); - let mut data = ciphertext; - data.extend(nonce_bytes); - - data -} - -fn decrypt_data(secret_key_bytes: [u8; 32], data: Vec) -> Vec { - let nonce_bytes = data[data.len() - 12..].to_vec(); - let encrypted_bytes = data[..data.len() - 12].to_vec(); - let key = Key::::from_slice(&secret_key_bytes); - let cipher = Aes256Gcm::new(key); - let nonce = GenericArray::from_slice(&nonce_bytes); - let decrypted_bytes = cipher - .decrypt(nonce, encrypted_bytes.as_ref()) - .expect("decryption failure!"); - - decrypted_bytes -} - -pub async fn encryptor( - our: String, - keypair: Arc, - message_tx: MessageSender, - mut recv_in_encryptor: MessageReceiver, - print_tx: PrintSender, -) -> Result<()> { - // Generally, the secret_id will be the ID that corresponds to a particular app or websocket connection - // For authenticated + encrypted HTTP routes, the secret_id will always be "http_bindings" - let mut secrets: HashMap = HashMap::new(); // Store secrets as hex strings? Or as bytes? - - while let Some(kernel_message) = recv_in_encryptor.recv().await { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "ENCRYPTOR MESSAGE".to_string(), - }) - .await; - let KernelMessage { - ref id, - source, - rsvp, - message, - payload, - .. - } = kernel_message; - let Message::Request(Request { ipc, .. }) = message else { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "encryptor: bad message".to_string(), - }) - .await; - continue; - }; - - match serde_json::from_slice::(&ipc) { - Ok(message) => { - match message { - EncryptorMessage::GetKey(GetKeyAction { - channel_id, - public_key_hex, - }) => { - let n = BigUint::from_str_radix(&public_key_hex.clone(), 16) - .expect("failed to parse hex string"); - let e = BigUint::from(65537u32); - - match RsaPublicKey::new(n, e) { - Ok(public_key) => { - let padding = Oaep::new::(); - let mut rng = rand::rngs::OsRng; - let public_key_bytes = hex::decode(public_key_hex) - .expect("failed to decode hex string"); - - let signed_public_key = - keypair.sign(&public_key_bytes).as_ref().to_vec(); - - let encrypted_secret: Vec; - if let Some(secret) = secrets.get(&channel_id) { - // Secret already exists - // Encrypt the secret with the public key and return it - encrypted_secret = public_key - .encrypt(&mut rng, padding, secret) - .expect("failed to encrypt message"); - } else { - // Secret does not exist, must create - // Create a new secret, store it, encrypt it with the public key, and return it - let mut secret = [0u8; 32]; - thread_rng().fill(&mut secret); - secrets.insert(channel_id, secret); - - // Create a new AES-GCM cipher with the given key - // So do I encrypt the - encrypted_secret = public_key - .encrypt(&mut rng, padding, &secret) - .expect("failed to encrypt message"); - } - - let mut headers = HashMap::new(); - headers.insert( - "Content-Type".to_string(), - "application/json".to_string(), - ); - - let target = match rsvp { - Some(rsvp) => rsvp, - None => Address { - node: source.node.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - }; - // Generate and send the response - let response = KernelMessage { - id: *id, - source: Address { - node: our.clone(), - process: ENCRYPTOR_PROCESS_ID.clone(), - }, - target, - rsvp: None, - message: Message::Response(( - Response { - inherit: false, - ipc: serde_json::json!({ - "status": 201, - "headers": headers, - }).to_string().into_bytes(), - metadata: None, - }, - None, - )), - payload: Some(Payload { - mime: Some("application/json".to_string()), - bytes: serde_json::json!({ - "encrypted_secret": hex::encode(encrypted_secret).to_string(), - "signed_public_key": hex::encode(&signed_public_key).to_string(), - }).to_string().as_bytes().to_vec(), - }), - signed_capabilities: None, - }; - - message_tx.send(response).await.unwrap(); - } - Err(e) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!("Error: {}", e), - }) - .await; - } - } - } - EncryptorMessage::DecryptAndForward(DecryptAndForwardAction { - channel_id, - forward_to, - json, - }) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!( - "DECRYPTOR TO FORWARD: {}", - json.clone().unwrap_or_default() - ), - }) - .await; - - // The payload.bytes should be the encrypted data, with the last 12 bytes being the nonce - let Some(payload) = payload else { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "No payload".to_string(), - }) - .await; - continue; - }; - - let data = payload.bytes.clone(); - - if let Some(secret_key_bytes) = secrets.get(&channel_id) { - let decrypted_bytes = decrypt_data(*secret_key_bytes, data); - - // Forward the unencrypted data to the target - let id: u64 = rand::random(); - let message = KernelMessage { - id, - source: Address { - node: our.clone(), - process: ENCRYPTOR_PROCESS_ID.clone(), - }, - target: forward_to, - rsvp: None, - message: Message::Request(Request { - inherit: false, - expects_response: None, // A forwarded message does not expect a response - ipc: json.unwrap_or_default().to_string().into_bytes(), - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), // TODO adjust MIME type as needed - bytes: decrypted_bytes, - }), - signed_capabilities: None, - }; - message_tx.send(message).await.unwrap(); - } else { - panic!("No secret found"); - } - } - EncryptorMessage::EncryptAndForward(EncryptAndForwardAction { - channel_id, - forward_to, - json, - }) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "ENCRYPTOR TO FORWARD".to_string(), - }) - .await; - - let Some(payload) = payload else { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "No payload".to_string(), - }) - .await; - continue; - }; - - let data = payload.bytes.clone(); - - if let Some(secret_key_bytes) = secrets.get(&channel_id) { - let encrypted_bytes = encrypt_data(*secret_key_bytes, data); - - // Forward the ciphertext and nonce_hex to the specified process - let id: u64 = rand::random(); - let message = KernelMessage { - id, - source: Address { - node: our.clone(), - process: ENCRYPTOR_PROCESS_ID.clone(), - }, - target: forward_to, - rsvp: None, - message: Message::Request(Request { - inherit: false, - expects_response: None, // A forwarded message does not expect a response - ipc: json.unwrap_or_default().to_string().into_bytes(), - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), // TODO adjust MIME type as needed - bytes: encrypted_bytes, - }), - signed_capabilities: None, - }; - - message_tx.send(message).await.unwrap(); - } else { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "ERROR: No secret found".to_string(), - }) - .await; - } - } - EncryptorMessage::Decrypt(DecryptAction { channel_id }) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "ENCRYPTOR TO DECRYPT".to_string(), - }) - .await; - - let Some(payload) = payload else { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "No payload".to_string(), - }) - .await; - continue; - }; - - let data = payload.bytes.clone(); - - if let Some(secret_key_bytes) = secrets.get(&channel_id) { - let decrypted_bytes = decrypt_data(*secret_key_bytes, data); - - let message = KernelMessage { - id: *id, - source: Address { - node: our.clone(), - process: ENCRYPTOR_PROCESS_ID.clone(), - }, - target: source, - rsvp: None, - message: Message::Response(( - Response { - inherit: false, - ipc: vec![], - metadata: None, - }, - None, - )), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), // TODO adjust MIME type as needed - bytes: decrypted_bytes, - }), - signed_capabilities: None, - }; - - message_tx.send(message).await.unwrap(); - } else { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "ERROR: No secret found".to_string(), - }) - .await; - } - } - EncryptorMessage::Encrypt(EncryptAction { channel_id }) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "ENCRYPTOR TO ENCRYPT".to_string(), - }) - .await; - - let Some(payload) = payload else { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "No payload".to_string(), - }) - .await; - continue; - }; - - let data = payload.bytes.clone(); - - if let Some(secret_key_bytes) = secrets.get(&channel_id) { - let encrypted_bytes = encrypt_data(*secret_key_bytes, data); - - let message = KernelMessage { - id: *id, - source: Address { - node: our.clone(), - process: ENCRYPTOR_PROCESS_ID.clone(), - }, - target: source, - rsvp: None, - message: Message::Response(( - Response { - inherit: false, - ipc: vec![], - metadata: None, - }, - None, - )), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), // TODO adjust MIME type as needed - bytes: encrypted_bytes, - }), - signed_capabilities: None, - }; - - message_tx.send(message).await.unwrap(); - } else { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "ERROR: No secret found".to_string(), - }) - .await; - } - } - } - } - Err(_) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "Not a valid EncryptorMessage".to_string(), - }) - .await; - } - } - } - Err(anyhow::anyhow!("encryptor: exited")) -} diff --git a/src/http/client.rs b/src/http/client.rs new file mode 100644 index 00000000..bcd85d53 --- /dev/null +++ b/src/http/client.rs @@ -0,0 +1,260 @@ +use crate::http::types::*; +use crate::types::*; +use anyhow::Result; +use http::header::{HeaderMap, HeaderName, HeaderValue}; +use std::collections::HashMap; +use std::sync::Arc; + +// Test http_client with these commands in the terminal +// !message our http_client {"method": "GET", "url": "https://jsonplaceholder.typicode.com/posts", "headers": {}} +// !message our http_client {"method": "POST", "url": "https://jsonplaceholder.typicode.com/posts", "headers": {"Content-Type": "application/json"}} +// !message our http_client {"method": "PUT", "url": "https://jsonplaceholder.typicode.com/posts", "headers": {"Content-Type": "application/json"}} + +pub async fn http_client( + our_name: String, + send_to_loop: MessageSender, + mut recv_in_client: MessageReceiver, + _print_tx: PrintSender, +) -> Result<()> { + let client = reqwest::Client::new(); + let our_name = Arc::new(our_name); + + while let Some(KernelMessage { + id, + source, + rsvp, + message: + Message::Request(Request { + expects_response, + ipc, + .. + }), + payload, + .. + }) = recv_in_client.recv().await + { + tokio::spawn(handle_message( + our_name.clone(), + id, + rsvp.unwrap_or(source), + expects_response, + ipc, + payload, + client.clone(), + send_to_loop.clone(), + )); + } + Err(anyhow::anyhow!("http_client: loop died")) +} + +async fn handle_message( + our: Arc, + id: u64, + target: Address, + expects_response: Option, + json: Vec, + body: Option, + client: reqwest::Client, + send_to_loop: MessageSender, +) { + let req: OutgoingHttpRequest = match serde_json::from_slice(&json) { + Ok(req) => req, + Err(_e) => { + make_error_message( + our, + id, + target, + expects_response, + HttpClientError::BadRequest { + req: String::from_utf8(json).unwrap_or_default(), + }, + send_to_loop, + ) + .await; + return; + } + }; + + let Ok(req_method) = http::Method::from_bytes(req.method.as_bytes()) else { + make_error_message( + our, + id, + target, + expects_response, + HttpClientError::BadMethod { method: req.method }, + send_to_loop, + ) + .await; + return; + }; + + let mut request_builder = client.request(req_method, req.url); + + if let Some(version) = req.version { + request_builder = match version.as_str() { + "HTTP/0.9" => request_builder.version(http::Version::HTTP_09), + "HTTP/1.0" => request_builder.version(http::Version::HTTP_10), + "HTTP/1.1" => request_builder.version(http::Version::HTTP_11), + "HTTP/2.0" => request_builder.version(http::Version::HTTP_2), + "HTTP/3.0" => request_builder.version(http::Version::HTTP_3), + _ => { + make_error_message( + our, + id, + target, + expects_response, + HttpClientError::BadVersion { version }, + send_to_loop, + ) + .await; + return; + } + } + } + + if let Some(payload) = body { + request_builder = request_builder.body(payload.bytes); + } + + let Ok(request) = request_builder + .headers(deserialize_headers(req.headers)) + .build() + else { + make_error_message( + our, + id, + target, + expects_response, + HttpClientError::RequestFailed { + error: "failed to build request".into(), + }, + send_to_loop, + ) + .await; + return; + }; + + match client.execute(request).await { + Ok(response) => { + if expects_response.is_some() { + let _ = send_to_loop + .send(KernelMessage { + id, + source: Address { + node: our.to_string(), + process: ProcessId::new(Some("http_client"), "sys", "uqbar"), + }, + target, + rsvp: None, + message: Message::Response(( + Response { + inherit: false, + ipc: serde_json::to_vec::>( + &Ok(HttpResponse { + status: response.status().as_u16(), + headers: serialize_headers(response.headers()), + }), + ) + .unwrap(), + metadata: None, + }, + None, + )), + payload: Some(Payload { + mime: None, + bytes: response.bytes().await.unwrap_or_default().to_vec(), + }), + signed_capabilities: None, + }) + .await; + } + } + Err(e) => { + make_error_message( + our, + id, + target, + expects_response, + HttpClientError::RequestFailed { + error: e.to_string(), + }, + send_to_loop, + ) + .await; + } + } +} + +// +// helpers +// + +fn to_pascal_case(s: &str) -> String { + s.split('-') + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + } + }) + .collect::>() + .join("-") +} + +fn serialize_headers(headers: &HeaderMap) -> HashMap { + let mut hashmap = HashMap::new(); + for (key, value) in headers.iter() { + let key_str = to_pascal_case(key.as_ref()); + let value_str = value.to_str().unwrap_or("").to_string(); + hashmap.insert(key_str, value_str); + } + hashmap +} + +fn deserialize_headers(hashmap: HashMap) -> HeaderMap { + let mut header_map = HeaderMap::new(); + for (key, value) in hashmap { + let key_bytes = key.as_bytes(); + let key_name = HeaderName::from_bytes(key_bytes).unwrap(); + let value_header = HeaderValue::from_str(&value).unwrap(); + header_map.insert(key_name, value_header); + } + header_map +} + +async fn make_error_message( + our: Arc, + id: u64, + target: Address, + expects_response: Option, + error: HttpClientError, + send_to_loop: MessageSender, +) { + if expects_response.is_some() { + let _ = send_to_loop + .send(KernelMessage { + id, + source: Address { + node: our.to_string(), + process: ProcessId::new(Some("http_client"), "sys", "uqbar"), + }, + target, + rsvp: None, + message: Message::Response(( + Response { + inherit: false, + ipc: serde_json::to_vec::>(&Err( + error, + )) + .unwrap(), + metadata: None, + }, + None, + )), + payload: None, + signed_capabilities: None, + }) + .await; + } +} diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 00000000..7546a732 --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod server; +pub mod types; +pub mod utils; diff --git a/src/http/server.rs b/src/http/server.rs new file mode 100644 index 00000000..bccbea86 --- /dev/null +++ b/src/http/server.rs @@ -0,0 +1,883 @@ +use crate::http::types::*; +use crate::http::utils::*; +use crate::register; +use crate::types::*; +use anyhow::Result; +use dashmap::DashMap; +use futures::{SinkExt, StreamExt}; +use route_recognizer::Router; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::sync::RwLock; +use warp::http::{header::HeaderValue, StatusCode}; +use warp::ws::{WebSocket, Ws}; +use warp::{Filter, Reply}; + +const HTTP_SELF_IMPOSED_TIMEOUT: u64 = 15; + +/// mapping from a given HTTP request (assigned an ID) to the oneshot +/// channel that will get a response from the app that handles the request, +/// and a string which contains the path that the request was made to. +type HttpResponseSenders = Arc>; +type HttpSender = tokio::sync::oneshot::Sender<(HttpResponse, Vec)>; + +/// mapping from an open websocket connection to a channel that will ingest +/// WebSocketPush messages from the app that handles the connection, and +/// send them to the connection. +type WebSocketSenders = Arc>; +type WebSocketSender = tokio::sync::mpsc::Sender; + +type PathBindings = Arc>>; + +struct BoundPath { + pub app: ProcessId, + pub authenticated: bool, + pub local_only: bool, + pub static_content: Option, // TODO store in filesystem and cache +} + +/// HTTP server: a runtime module that handles HTTP requests at a given port. +/// The server accepts bindings-requests from apps. These can be used in two ways: +/// +/// 1. The app can bind to a path and receive all subsequent requests in the form +/// of an [`HttpRequest`] to that path. +/// They will be responsible for generating HTTP responses in the form of an +/// [`HttpResponse`] to those requests. +/// +/// 2. The app can bind static content to a path. The server will handle all subsequent +/// requests, serving that static content. It will only respond to `GET` requests. +/// +/// +/// In addition to binding on paths, the HTTP server can receive incoming WebSocket connections +/// and pass them to a targeted app. The server will handle encrypting and decrypting messages +/// over these connections. +pub async fn http_server( + our_name: String, + our_port: u16, + jwt_secret_bytes: Vec, + mut recv_in_server: MessageReceiver, + send_to_loop: MessageSender, + print_tx: PrintSender, +) -> Result<()> { + let our_name = Arc::new(our_name); + let jwt_secret_bytes = Arc::new(jwt_secret_bytes); + let http_response_senders: HttpResponseSenders = Arc::new(DashMap::new()); + let ws_senders: WebSocketSenders = Arc::new(DashMap::new()); + + // Add RPC path + let mut bindings_map: Router = Router::new(); + let rpc_bound_path = BoundPath { + app: ProcessId::from_str("rpc:sys:uqbar").unwrap(), + authenticated: false, + local_only: true, + static_content: None, + }; + bindings_map.add("/rpc:sys:uqbar/message", rpc_bound_path); + + let path_bindings: PathBindings = Arc::new(RwLock::new(bindings_map)); + + tokio::spawn(serve( + our_name.clone(), + our_port, + http_response_senders.clone(), + path_bindings.clone(), + ws_senders.clone(), + jwt_secret_bytes.clone(), + send_to_loop.clone(), + print_tx.clone(), + )); + + while let Some(km) = recv_in_server.recv().await { + // we *can* move this into a dedicated task, but it's not necessary + handle_app_message( + km, + http_response_senders.clone(), + path_bindings.clone(), + ws_senders.clone(), + jwt_secret_bytes.clone(), + send_to_loop.clone(), + print_tx.clone(), + ) + .await; + } + Err(anyhow::anyhow!("http_server: http_server loop exited")) +} + +/// The 'server' part. Listens on a port assigned by runtime, and handles +/// all HTTP requests on it. Also allows incoming websocket connections. +async fn serve( + our: Arc, + our_port: u16, + http_response_senders: HttpResponseSenders, + path_bindings: PathBindings, + ws_senders: WebSocketSenders, + jwt_secret_bytes: Arc>, + send_to_loop: MessageSender, + print_tx: PrintSender, +) { + let _ = print_tx + .send(Printout { + verbosity: 0, + content: format!("http_server: running on port {}", our_port), + }) + .await; + + // Filter to receive websockets + let cloned_msg_tx = send_to_loop.clone(); + let cloned_our = our.clone(); + let cloned_jwt_secret_bytes = jwt_secret_bytes.clone(); + let ws_route = warp::path::end() + .and(warp::ws()) + .and(warp::any().map(move || cloned_our.clone())) + .and(warp::any().map(move || cloned_jwt_secret_bytes.clone())) + .and(warp::any().map(move || ws_senders.clone())) + .and(warp::any().map(move || cloned_msg_tx.clone())) + .map( + |ws_connection: Ws, + our: Arc, + jwt_secret_bytes: Arc>, + ws_senders: WebSocketSenders, + send_to_loop: MessageSender| { + ws_connection.on_upgrade(move |ws: WebSocket| async move { + maintain_websocket(ws, our, jwt_secret_bytes, ws_senders, send_to_loop).await + }) + }, + ); + // Filter to receive HTTP requests + let filter = warp::filters::method::method() + .and(warp::addr::remote()) + .and(warp::path::full()) + .and(warp::filters::header::headers_cloned()) + .and(warp::filters::body::bytes()) + .and(warp::any().map(move || our.clone())) + .and(warp::any().map(move || http_response_senders.clone())) + .and(warp::any().map(move || path_bindings.clone())) + .and(warp::any().map(move || jwt_secret_bytes.clone())) + .and(warp::any().map(move || send_to_loop.clone())) + .and_then(http_handler); + + let filter_with_ws = ws_route.or(filter); + warp::serve(filter_with_ws) + .run(([0, 0, 0, 0], our_port)) + .await; +} + +async fn http_handler( + method: warp::http::Method, + socket_addr: Option, + path: warp::path::FullPath, + headers: warp::http::HeaderMap, + body: warp::hyper::body::Bytes, + our: Arc, + http_response_senders: HttpResponseSenders, + path_bindings: PathBindings, + jwt_secret_bytes: Arc>, + send_to_loop: MessageSender, +) -> Result { + // TODO this is all so dirty. Figure out what actually matters. + + println!( + "http_server: got request from {:?} for {}\r", + socket_addr, + path.as_str() + ); + + // trim trailing "/" + let original_path = normalize_path(path.as_str()); + let id: u64 = rand::random(); + let serialized_headers = serialize_headers(&headers); + let path_bindings = path_bindings.read().await; + + let Ok(route) = path_bindings.recognize(&original_path) else { + return Ok(warp::reply::with_status(vec![], StatusCode::NOT_FOUND).into_response()); + }; + let bound_path = route.handler(); + + println!("here1\r"); + + if bound_path.authenticated { + let auth_token = serialized_headers + .get("cookie") + .cloned() + .unwrap_or_default(); + if !auth_cookie_valid(&our, &auth_token, &jwt_secret_bytes) { + return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response()); + } + } + + println!("here2\r"); + + let is_local = socket_addr + .map(|addr| addr.ip().is_loopback()) + .unwrap_or(false); + + if bound_path.local_only && !is_local { + return Ok(warp::reply::with_status(vec![], StatusCode::FORBIDDEN).into_response()); + } + + println!("here3\r"); + + // if path has static content, serve it + if let Some(static_content) = &bound_path.static_content { + return Ok(warp::http::Response::builder() + .status(StatusCode::OK) + .header( + "Content-Type", + static_content + .mime + .as_ref() + .unwrap_or(&"text/plain".to_string()), + ) + .body(static_content.bytes.clone()) + .into_response()); + } + + println!("here4\r"); + + // RPC functionality: if path is /rpc:sys:uqbar/message, + // we extract message from base64 encoded bytes in data + // and send it to the correct app. + let message = if bound_path.app == "rpc:sys:uqbar" { + match handle_rpc_message(our, id, body).await { + Ok(message) => message, + Err(e) => { + return Ok(warp::reply::with_status(vec![], e).into_response()); + } + } + } else { + // otherwise, make a message to the correct app + KernelMessage { + id, + source: Address { + node: our.to_string(), + process: HTTP_SERVER_PROCESS_ID.clone(), + }, + target: Address { + node: our.to_string(), + process: bound_path.app.clone(), + }, + rsvp: None, + message: Message::Request(Request { + inherit: false, + expects_response: Some(HTTP_SELF_IMPOSED_TIMEOUT), + 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), + headers: serialized_headers, + }) + .unwrap(), + metadata: None, + }), + payload: Some(Payload { + mime: None, + bytes: body.to_vec(), + }), + signed_capabilities: None, + } + }; + + let (response_sender, response_receiver) = tokio::sync::oneshot::channel(); + http_response_senders.insert(id, (original_path, response_sender)); + + match send_to_loop.send(message).await { + Ok(_) => {} + Err(_) => { + return Ok( + warp::reply::with_status(vec![], StatusCode::INTERNAL_SERVER_ERROR).into_response(), + ); + } + } + + let timeout_duration = tokio::time::Duration::from_secs(HTTP_SELF_IMPOSED_TIMEOUT); + let result = tokio::time::timeout(timeout_duration, response_receiver).await; + + let (http_response, body) = match result { + Ok(Ok(res)) => res, + Ok(Err(_)) => { + return Ok( + warp::reply::with_status(vec![], StatusCode::INTERNAL_SERVER_ERROR).into_response(), + ); + } + Err(_) => { + return Ok( + warp::reply::with_status(vec![], StatusCode::REQUEST_TIMEOUT).into_response(), + ); + } + }; + + let reply = warp::reply::with_status( + body, + StatusCode::from_u16(http_response.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + ); + let mut response = reply.into_response(); + + // Merge the deserialized headers into the existing headers + let existing_headers = response.headers_mut(); + for (header_name, header_value) in deserialize_headers(http_response.headers).iter() { + if header_name == "set-cookie" || header_name == "Set-Cookie" { + if let Ok(cookie) = header_value.to_str() { + let cookie_headers: Vec<&str> = cookie + .split("; ") + .filter(|&cookie| !cookie.is_empty()) + .collect(); + for cookie_header in cookie_headers { + if let Ok(valid_cookie) = HeaderValue::from_str(cookie_header) { + existing_headers.append(header_name, valid_cookie); + } + } + } + } else { + existing_headers.insert(header_name.to_owned(), header_value.to_owned()); + } + } + Ok(response) +} + +async fn handle_rpc_message( + our: Arc, + id: u64, + body: warp::hyper::body::Bytes, +) -> Result { + let Ok(rpc_message) = serde_json::from_slice::(&body) else { + return Err(StatusCode::BAD_REQUEST); + }; + + let Ok(target_process) = ProcessId::from_str(&rpc_message.process) else { + return Err(StatusCode::BAD_REQUEST); + }; + + let payload: Option = match rpc_message.data { + None => None, + Some(b64_bytes) => match base64::decode(b64_bytes) { + Ok(bytes) => Some(Payload { + mime: rpc_message.mime, + bytes, + }), + Err(_) => None, + }, + }; + + Ok(KernelMessage { + id, + source: Address { + node: our.to_string(), + process: HTTP_SERVER_PROCESS_ID.clone(), + }, + target: Address { + node: rpc_message.node.unwrap_or(our.to_string()), + process: target_process, + }, + rsvp: Some(Address { + node: our.to_string(), + process: HTTP_SERVER_PROCESS_ID.clone(), + }), + message: Message::Request(Request { + inherit: false, + expects_response: Some(15), // NB: no effect on runtime + ipc: match rpc_message.ipc { + Some(ipc_string) => ipc_string.into_bytes(), + None => Vec::new(), + }, + metadata: rpc_message.metadata, + }), + payload, + signed_capabilities: None, + }) +} + +async fn maintain_websocket( + ws: WebSocket, + our: Arc, + jwt_secret_bytes: Arc>, + ws_senders: WebSocketSenders, + send_to_loop: MessageSender, +) { + let (mut write_stream, mut read_stream) = ws.split(); + + // first, receive a message from client that contains the target process + // and the auth token + + let Some(Ok(register_msg)) = read_stream.next().await else { + // stream closed, exit + let stream = write_stream.reunite(read_stream).unwrap(); + let _ = stream.close().await; + return; + }; + + let Ok(ws_register) = serde_json::from_slice::(register_msg.as_bytes()) else { + // stream error, exit + let stream = write_stream.reunite(read_stream).unwrap(); + let _ = stream.close().await; + return; + }; + + let Ok(owner_process) = ProcessId::from_str(&ws_register.target_process) else { + // invalid process id, exit + let stream = write_stream.reunite(read_stream).unwrap(); + let _ = stream.close().await; + return; + }; + + let Ok(our_name) = verify_auth_token(&ws_register.auth_token, &jwt_secret_bytes) else { + // invalid auth token, exit + let stream = write_stream.reunite(read_stream).unwrap(); + let _ = stream.close().await; + return; + }; + + if our_name != *our { + // invalid auth token, exit + let stream = write_stream.reunite(read_stream).unwrap(); + let _ = stream.close().await; + return; + } + + let ws_channel_id: u64 = rand::random(); + let (ws_sender, mut ws_receiver) = tokio::sync::mpsc::channel(100); + ws_senders.insert(ws_channel_id, (owner_process.clone(), ws_sender)); + + // respond to the client notifying them that the channel is now open + let Ok(()) = write_stream + .send(warp::ws::Message::text( + serde_json::to_string(&WsRegisterResponse { + channel_id: ws_channel_id, + }) + .unwrap(), + )) + .await + else { + // stream error, exit + let stream = write_stream.reunite(read_stream).unwrap(); + let _ = stream.close().await; + return; + }; + + loop { + tokio::select! { + read = read_stream.next() => { + match read { + None => { + // stream closed, remove and exit + websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await; + break; + } + Some(Err(_e)) => { + // stream error, remove and exit + websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await; + break; + } + Some(Ok(msg)) => { + // forward message to process associated with this channel + let _ = send_to_loop + .send(KernelMessage { + id: rand::random(), + source: Address { + node: our.to_string(), + process: HTTP_SERVER_PROCESS_ID.clone(), + }, + target: Address { + node: our.to_string(), + process: owner_process.clone(), + }, + rsvp: None, + message: Message::Request(Request { + inherit: false, + expects_response: None, + ipc: serde_json::to_vec(&HttpServerAction::WebSocketPush { + channel_id: ws_channel_id, + message_type: WsMessageType::Binary, + }).unwrap(), + metadata: None, + }), + payload: Some(Payload { + mime: None, + bytes: msg.into_bytes(), + }), + signed_capabilities: None, + }) + .await; + } + } + } + Some(outgoing) = ws_receiver.recv() => { + // forward message to websocket + match write_stream.send(outgoing).await { + Ok(()) => continue, + Err(_e) => { + // stream error, remove and exit + websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await; + break; + } + } + } + } + } + let stream = write_stream.reunite(read_stream).unwrap(); + let _ = stream.close().await; +} + +async fn websocket_close( + channel_id: u64, + process: ProcessId, + ws_senders: &WebSocketSenders, + send_to_loop: &MessageSender, +) { + ws_senders.remove(&channel_id); + let _ = send_to_loop + .send(KernelMessage { + id: rand::random(), + source: Address { + node: "our".to_string(), + process: HTTP_SERVER_PROCESS_ID.clone(), + }, + target: Address { + node: "our".to_string(), + process, + }, + rsvp: None, + message: Message::Request(Request { + inherit: false, + expects_response: None, + ipc: serde_json::to_vec(&HttpServerAction::WebSocketClose(channel_id)).unwrap(), + metadata: None, + }), + payload: Some(Payload { + mime: None, + bytes: serde_json::to_vec(&RpcResponseBody { + ipc: Vec::new(), + payload: None, + }) + .unwrap(), + }), + signed_capabilities: None, + }) + .await; +} + +async fn handle_app_message( + km: KernelMessage, + http_response_senders: HttpResponseSenders, + path_bindings: PathBindings, + ws_senders: WebSocketSenders, + jwt_secret_bytes: Arc>, + send_to_loop: MessageSender, + print_tx: PrintSender, +) { + // when we get a Response, try to match it to an outstanding HTTP + // request and send it there. + // when we get a Request, parse it into an HttpServerAction and perform it. + match km.message { + Message::Response((response, _context)) => { + let Some((_id, (path, sender))) = http_response_senders.remove(&km.id) else { + return; + }; + // if path is /rpc/message, return accordingly with base64 encoded payload + if path == "/rpc:sys:uqbar/message" { + let payload = km.payload.map(|p| Payload { + mime: p.mime, + bytes: base64::encode(p.bytes).into_bytes(), + }); + + let mut default_headers = HashMap::new(); + default_headers.insert("Content-Type".to_string(), "text/html".to_string()); + + let _ = sender.send(( + HttpResponse { + status: 200, + headers: default_headers, + }, + serde_json::to_vec(&RpcResponseBody { + ipc: response.ipc, + payload, + }) + .unwrap(), + )); + } else { + let Ok(mut response) = serde_json::from_slice::(&response.ipc) else { + // the receiver will automatically trigger a 503 when sender is dropped. + return; + }; + let Some((_id, (path, channel))) = http_response_senders.remove(&km.id) else { + return; + }; + // XX REFACTOR THIS: + // for the login case, todo refactor out? + let segments: Vec<&str> = path + .split('/') + .filter(|&segment| !segment.is_empty()) + .collect(); + // If we're getting back a /login from a proxy (or our own node), + // then we should generate a jwt from the secret + the name of the ship, + // and then attach it to a header. + if response.status < 400 + && (segments.len() == 1 || segments.len() == 4) + && matches!(segments.last(), Some(&"login")) + { + if let Some(auth_cookie) = response.headers.get("set-cookie") { + let mut ws_auth_username = km.source.node.clone(); + + if segments.len() == 4 + && matches!(segments.first(), Some(&"http-proxy")) + && matches!(segments.get(1), Some(&"serve")) + { + if let Some(segment) = segments.get(2) { + ws_auth_username = segment.to_string(); + } + } + if let Some(token) = register::generate_jwt( + jwt_secret_bytes.to_vec().as_slice(), + ws_auth_username.clone(), + ) { + let auth_cookie_with_ws = format!( + "{}; uqbar-ws-auth_{}={};", + auth_cookie, + ws_auth_username.clone(), + token + ); + response + .headers + .insert("set-cookie".to_string(), auth_cookie_with_ws); + + let _ = print_tx + .send(Printout { + verbosity: 1, + content: format!( + "SET WS AUTH COOKIE WITH USERNAME: {}", + ws_auth_username + ), + }) + .await; + } + } + } + let _ = channel.send(( + HttpResponse { + status: response.status, + headers: response.headers, + }, + match km.payload { + None => vec![], + Some(p) => p.bytes, + }, + )); + } + } + Message::Request(Request { ref ipc, .. }) => { + let Ok(message) = serde_json::from_slice::(ipc) else { + println!( + "http_server: got malformed request from {}: {:?}\r", + km.source, ipc + ); + send_action_response( + km.id, + km.source, + &send_to_loop, + Err(HttpServerError::BadRequest { + req: String::from_utf8_lossy(ipc).to_string(), + }), + ) + .await; + return; + }; + match message { + HttpServerAction::Bind { + mut path, + authenticated, + local_only, + cache, + } => { + let mut path_bindings = path_bindings.write().await; + if km.source.process != "homepage:homepage:uqbar" { + path = if path.starts_with('/') { + format!("/{}{}", km.source.process, path) + } else { + format!("/{}/{}", km.source.process, path) + }; + } + if !cache { + // trim trailing "/" + path_bindings.add( + &normalize_path(&path), + BoundPath { + app: km.source.process.clone(), + authenticated, + local_only, + 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(), + authenticated, + local_only, + static_content: Some(payload), + }, + ); + } + send_action_response(km.id, km.source, &send_to_loop, Ok(())).await; + } + HttpServerAction::WebSocketOpen(_) => { + // we cannot receive these, only send them to processes + send_action_response( + km.id, + km.source, + &send_to_loop, + Err(HttpServerError::WebSocketPushError { + error: "WebSocketOpen is not a valid request".to_string(), + }), + ) + .await; + } + HttpServerAction::WebSocketPush { + channel_id, + message_type, + } => { + let Some(payload) = km.payload else { + send_action_response( + km.id, + km.source, + &send_to_loop, + Err(HttpServerError::NoPayload), + ) + .await; + return; + }; + let ws_message = match message_type { + WsMessageType::Text => warp::ws::Message::text( + String::from_utf8_lossy(&payload.bytes).to_string(), + ), + WsMessageType::Binary => warp::ws::Message::binary(payload.bytes), + WsMessageType::Ping | WsMessageType::Pong => { + if payload.bytes.len() > 125 { + send_action_response( + km.id, + km.source, + &send_to_loop, + Err(HttpServerError::WebSocketPushError { + error: "Ping and Pong messages must be 125 bytes or less" + .to_string(), + }), + ) + .await; + return; + } + if message_type == WsMessageType::Ping { + warp::ws::Message::ping(payload.bytes) + } else { + warp::ws::Message::pong(payload.bytes) + } + } + }; + // Send to the websocket if registered + if let Some(got) = ws_senders.get(&channel_id) { + let owner_process = &got.value().0; + let sender = &got.value().1; + if owner_process != &km.source.process { + send_action_response( + km.id, + km.source, + &send_to_loop, + Err(HttpServerError::WebSocketPushError { + error: "WebSocket channel not owned by this process" + .to_string(), + }), + ) + .await; + return; + } + match sender.send(ws_message).await { + Ok(_) => { + send_action_response(km.id, km.source, &send_to_loop, Ok(())).await; + } + Err(_) => { + send_action_response( + km.id, + km.source, + &send_to_loop, + Err(HttpServerError::WebSocketPushError { + error: "WebSocket channel closed".to_string(), + }), + ) + .await; + } + } + } else { + send_action_response( + km.id, + km.source, + &send_to_loop, + Err(HttpServerError::WebSocketPushError { + error: "WebSocket channel not found".to_string(), + }), + ) + .await; + } + } + HttpServerAction::WebSocketClose(channel_id) => { + if let Some(got) = ws_senders.get(&channel_id) { + if got.value().0 != km.source.process { + send_action_response( + km.id, + km.source, + &send_to_loop, + Err(HttpServerError::WebSocketPushError { + error: "WebSocket channel not owned by this process" + .to_string(), + }), + ) + .await; + return; + } + let _ = got.value().1.send(warp::ws::Message::close()).await; + ws_senders.remove(&channel_id); + send_action_response(km.id, km.source, &send_to_loop, Ok(())).await; + } + } + } + } + } +} + +pub async fn send_action_response( + id: u64, + target: Address, + send_to_loop: &MessageSender, + result: Result<(), HttpServerError>, +) { + let _ = send_to_loop + .send(KernelMessage { + id, + source: Address { + node: "our".to_string(), + process: HTTP_SERVER_PROCESS_ID.clone(), + }, + target, + rsvp: None, + message: Message::Response(( + Response { + inherit: false, + ipc: serde_json::to_vec(&result).unwrap(), + metadata: None, + }, + None, + )), + payload: None, + signed_capabilities: None, + }) + .await; +} diff --git a/src/http/types.rs b/src/http/types.rs new file mode 100644 index 00000000..5486bd36 --- /dev/null +++ b/src/http/types.rs @@ -0,0 +1,140 @@ +use crate::types::Payload; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use thiserror::Error; + +/// HTTP Request type that can be shared over WASM boundary to apps. +/// This is the one you receive from the `http_server:sys:uqbar` service. +#[derive(Debug, Serialize, Deserialize)] +pub struct IncomingHttpRequest { + pub source_socket_addr: Option, // will parse to SocketAddr + pub method: String, // will parse to http::Method + pub raw_path: String, + pub headers: HashMap, + // BODY is stored in the payload, as bytes +} + +/// HTTP Request type that can be shared over WASM boundary to apps. +/// This is the one you send to the `http_client:sys:uqbar` service. +#[derive(Debug, Serialize, Deserialize)] +pub struct OutgoingHttpRequest { + pub method: String, // must parse to http::Method + pub version: Option, // must parse to http::Version + pub url: String, // must parse to url::Url + pub headers: HashMap, + // BODY is stored in the payload, as bytes + // TIMEOUT is stored in the message expect_response +} + +/// HTTP Response type that can be shared over WASM boundary to apps. +/// Respond to [`IncomingHttpRequest`] with this type. +#[derive(Debug, Serialize, Deserialize)] +pub struct HttpResponse { + pub status: u16, + pub headers: HashMap, + // BODY is stored in the payload, as bytes +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RpcResponseBody { + pub ipc: Vec, + pub payload: Option, +} + +#[derive(Error, Debug, Serialize, Deserialize)] +pub enum HttpClientError { + #[error("http_client: request could not be parsed to HttpRequest: {}.", req)] + BadRequest { req: String }, + #[error("http_client: http method not supported: {}", method)] + BadMethod { method: String }, + #[error("http_client: url could not be parsed: {}", url)] + BadUrl { url: String }, + #[error("http_client: http version not supported: {}", version)] + BadVersion { version: String }, + #[error("http_client: failed to execute request {}", error)] + RequestFailed { error: String }, +} + +/// Request type sent to `http_server:sys:uqbar` in order to configure it. +/// You can also send [`WebSocketPush`], which allows you to push messages +/// across an existing open WebSocket connection. +/// +/// If a response is expected, all HttpServerActions will return a Response +/// with the shape Result<(), HttpServerActionError> serialized to JSON. +#[derive(Debug, Serialize, Deserialize)] +pub enum HttpServerAction { + /// Bind expects a payload if and only if `cache` is TRUE. The payload should + /// be the static file to serve at this path. + Bind { + path: String, + authenticated: bool, + local_only: bool, + cache: bool, + }, + /// Processes will RECEIVE this kind of request when a client connects to them. + /// If a process does not want this websocket open, they can respond with an + /// [`enum@HttpServerAction::WebSocketClose`] message. + WebSocketOpen(u64), + /// Processes can both SEND and RECEIVE this kind of request. + /// When sent, expects a payload containing the WebSocket message bytes to send. + WebSocketPush { + channel_id: u64, + message_type: WsMessageType, + }, + /// Processes can both SEND and RECEIVE this kind of request. Sending will + /// close a socket the process controls. Receiving will indicate that the + /// client closed the socket. + WebSocketClose(u64), +} + +/// The possible message types for WebSocketPush. Ping and Pong are limited to 125 bytes +/// by the WebSockets protocol. Text will be sent as a Text frame, with the payload bytes +/// being the UTF-8 encoding of the string. Binary will be sent as a Binary frame containing +/// the unmodified payload bytes. +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum WsMessageType { + Text, + Binary, + Ping, + Pong, +} + +/// Part of the Response type issued by http_server +#[derive(Error, Debug, Serialize, Deserialize)] +pub enum HttpServerError { + #[error( + "http_server: request could not be parsed to HttpServerAction: {}.", + req + )] + BadRequest { req: String }, + #[error("http_server: action expected payload")] + NoPayload, + #[error("http_server: path binding error: {:?}", error)] + PathBindError { error: String }, + #[error("http_server: WebSocket error: {:?}", error)] + WebSocketPushError { error: String }, +} + +/// Structure sent from client websocket to this server upon opening a new connection. +/// After this is sent, depending on the `encrypted` flag, the channel will either be +/// open to send and receive plaintext messages or messages encrypted with a symmetric +/// key derived from the JWT. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WsRegister { + pub auth_token: String, + pub target_process: String, + pub encrypted: bool, // TODO symmetric key exchange here if true +} + +/// Structure sent from this server to client websocket upon opening a new connection. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WsRegisterResponse { + pub channel_id: u64, + // TODO symmetric key exchange here +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JwtClaims { + pub username: String, + pub expiration: u64, +} diff --git a/src/http/utils.rs b/src/http/utils.rs new file mode 100644 index 00000000..f33202b8 --- /dev/null +++ b/src/http/utils.rs @@ -0,0 +1,116 @@ +use crate::http::types::*; +use hmac::{Hmac, Mac}; +use jwt::VerifyWithKey; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use std::collections::HashMap; +use tokio::net::TcpListener; +use warp::http::{header::HeaderName, header::HeaderValue, HeaderMap}; + +#[derive(Serialize, Deserialize)] +pub struct RpcMessage { + pub node: Option, + pub process: String, + pub inherit: Option, + pub expects_response: Option, + pub ipc: Option, + pub metadata: Option, + pub context: Option, + pub mime: Option, + pub data: Option, +} + +/// Ingest an auth token given from client and return the node name or an error. +pub fn verify_auth_token(auth_token: &str, jwt_secret: &[u8]) -> Result { + let Ok(secret) = Hmac::::new_from_slice(jwt_secret) else { + return Err(jwt::Error::Format); + }; + + let claims: Result = auth_token.verify_with_key(&secret); + + match claims { + Ok(data) => Ok(data.username), + Err(err) => Err(err), + } +} + +pub fn auth_cookie_valid(our_node: &str, cookie: &str, jwt_secret: &[u8]) -> bool { + let cookie_parts: Vec<&str> = cookie.split("; ").collect(); + let mut auth_token = None; + + 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!("uqbar-auth_{}", our_node) + { + auth_token = Some(cookie_part_parts[1].to_string()); + break; + } + } + + let auth_token = match auth_token { + Some(token) if !token.is_empty() => token, + _ => return false, + }; + + let Ok(secret) = Hmac::::new_from_slice(jwt_secret) else { + return false; + }; + + let claims: Result = auth_token.verify_with_key(&secret); + + match claims { + Ok(data) => data.username == our_node, + Err(_) => false, + } +} + +pub fn normalize_path(path: &str) -> String { + match path.strip_suffix('/') { + Some(new) => new.to_string(), + None => path.to_string(), + } +} + +pub fn serialize_headers(headers: &HeaderMap) -> HashMap { + let mut hashmap = HashMap::new(); + for (key, value) in headers.iter() { + let key_str = key.to_string(); + let value_str = value.to_str().unwrap_or("").to_string(); + hashmap.insert(key_str, value_str); + } + hashmap +} + +pub fn deserialize_headers(hashmap: HashMap) -> HeaderMap { + let mut header_map = HeaderMap::new(); + for (key, value) in hashmap { + let key_bytes = key.as_bytes(); + let Ok(key_name) = HeaderName::from_bytes(key_bytes) else { + continue; + }; + let Ok(value_header) = HeaderValue::from_str(&value) else { + continue; + }; + header_map.insert(key_name, value_header); + } + header_map +} + +pub async fn find_open_port(start_at: u16) -> Option { + for port in start_at..(start_at + 1000) { + let bind_addr = format!("0.0.0.0:{}", port); + if is_port_available(&bind_addr).await { + return Some(port); + } + } + None +} + +pub async fn is_port_available(bind_addr: &str) -> bool { + TcpListener::bind(bind_addr).await.is_ok() +} + +pub fn _binary_encoded_string_to_bytes(s: &str) -> Vec { + s.chars().map(|c| c as u8).collect() +} diff --git a/src/http_client.rs b/src/http_client.rs deleted file mode 100644 index 89eb1961..00000000 --- a/src/http_client.rs +++ /dev/null @@ -1,227 +0,0 @@ -use crate::types::*; -use anyhow::Result; -use http::header::{HeaderMap, HeaderName, HeaderValue}; -use std::collections::HashMap; - -// Test http_client with these commands in the terminal -// !message tuna http_client {"method": "GET", "uri": "https://jsonplaceholder.typicode.com/posts", "headers": {}, "body": ""} -// !message tuna http_client {"method": "POST", "uri": "https://jsonplaceholder.typicode.com/posts", "headers": {"Content-Type": "application/json"}, "body": "{\"title\": \"foo\", \"body\": \"bar\"}"} -// !message tuna http_client {"method": "PUT", "uri": "https://jsonplaceholder.typicode.com/posts", "headers": {"Content-Type": "application/json"}, "body": "{\"title\": \"foo\", \"body\": \"bar\"}"} - -pub async fn http_client( - our_name: String, - send_to_loop: MessageSender, - mut recv_in_client: MessageReceiver, - print_tx: PrintSender, -) -> Result<()> { - while let Some(message) = recv_in_client.recv().await { - let KernelMessage { - id, - source, - rsvp, - message: - Message::Request(Request { - expects_response, - ipc, - .. - }), - payload, - .. - } = message.clone() - else { - return Err(anyhow::anyhow!("http_client: bad message")); - }; - - let our_name = our_name.clone(); - let send_to_loop = send_to_loop.clone(); - let print_tx = print_tx.clone(); - - tokio::spawn(async move { - if let Err(e) = handle_message( - our_name.clone(), - send_to_loop.clone(), - id, - rsvp, - expects_response, - source.clone(), - ipc, - { - if let Some(payload) = payload { - Some(payload.bytes) - } else { - None - } - }, - print_tx.clone(), - ) - .await - { - send_to_loop - .send(make_error_message(our_name.clone(), id, source, e)) - .await - .unwrap(); - } - }); - } - Err(anyhow::anyhow!("http_client: exited")) -} - -async fn handle_message( - our: String, - send_to_loop: MessageSender, - id: u64, - rsvp: Option
    , - expects_response: Option, - source: Address, - json: Vec, - body: Option>, - _print_tx: PrintSender, -) -> Result<(), HttpClientError> { - let target = if expects_response.is_some() { - source.clone() - } else { - let Some(rsvp) = rsvp else { - return Err(HttpClientError::BadRsvp); - }; - rsvp.clone() - }; - - let req: HttpClientRequest = match serde_json::from_slice(&json) { - Ok(req) => req, - Err(e) => { - return Err(HttpClientError::BadJson { - json: String::from_utf8(json).unwrap_or_default(), - error: format!("{}", e), - }) - } - }; - - let client = reqwest::Client::new(); - - let request_builder = match req.method.to_uppercase()[..].to_string().as_str() { - "GET" => client.get(req.uri), - "PUT" => client.put(req.uri), - "POST" => client.post(req.uri), - "DELETE" => client.delete(req.uri), - method => { - return Err(HttpClientError::BadMethod { - method: method.into(), - }); - } - }; - - let request = request_builder - .headers(deserialize_headers(req.headers)) - .body(body.unwrap_or_default()) - .build() - .unwrap(); - - let response = match client.execute(request).await { - Ok(response) => response, - Err(e) => { - return Err(HttpClientError::RequestFailed { - error: format!("{}", e), - }); - } - }; - - let http_client_response = HttpClientResponse { - status: response.status().as_u16(), - headers: serialize_headers(&response.headers().clone()), - }; - - let message = KernelMessage { - id, - source: Address { - node: our, - process: ProcessId::new(Some("http_client"), "sys", "uqbar"), - }, - target, - rsvp: None, - message: Message::Response(( - Response { - inherit: false, - ipc: serde_json::to_vec::>(&Ok( - http_client_response, - )) - .unwrap(), - metadata: None, - }, - None, - )), - payload: Some(Payload { - mime: Some("application/json".into()), - bytes: response.bytes().await.unwrap().to_vec(), - }), - signed_capabilities: None, - }; - - send_to_loop.send(message).await.unwrap(); - - Ok(()) -} - -// -// helpers -// -fn to_pascal_case(s: &str) -> String { - s.split('-') - .map(|word| { - let mut chars = word.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - } - }) - .collect::>() - .join("-") -} - -fn serialize_headers(headers: &HeaderMap) -> HashMap { - let mut hashmap = HashMap::new(); - for (key, value) in headers.iter() { - let key_str = to_pascal_case(key.as_ref()); - let value_str = value.to_str().unwrap_or("").to_string(); - hashmap.insert(key_str, value_str); - } - hashmap -} - -fn deserialize_headers(hashmap: HashMap) -> HeaderMap { - let mut header_map = HeaderMap::new(); - for (key, value) in hashmap { - let key_bytes = key.as_bytes(); - let key_name = HeaderName::from_bytes(key_bytes).unwrap(); - let value_header = HeaderValue::from_str(&value).unwrap(); - header_map.insert(key_name, value_header); - } - header_map -} - -fn make_error_message( - our_name: String, - id: u64, - source: Address, - error: HttpClientError, -) -> KernelMessage { - KernelMessage { - id, - source: source.clone(), - target: Address { - node: our_name.clone(), - process: source.process.clone(), - }, - rsvp: None, - message: Message::Response(( - Response { - inherit: false, - ipc: serde_json::to_vec::>(&Err(error)) - .unwrap(), - metadata: None, - }, - None, - )), - payload: None, - signed_capabilities: None, - } -} diff --git a/src/http_server/mod.rs b/src/http_server/mod.rs deleted file mode 100644 index f49645ec..00000000 --- a/src/http_server/mod.rs +++ /dev/null @@ -1,940 +0,0 @@ -use crate::http_server::server_fns::*; -use crate::register; -use crate::types::*; -use anyhow::Result; - -use futures::SinkExt; -use futures::StreamExt; - -use route_recognizer::Router; -use std::collections::HashMap; -use std::net::SocketAddr; -use std::sync::Arc; -use tokio::sync::oneshot; -use tokio::sync::Mutex; -use tokio::sync::RwLock; -use warp::http::{header::HeaderValue, StatusCode}; -use warp::ws::{WebSocket, Ws}; -use warp::{Filter, Reply}; - -mod server_fns; - -// types and constants -type HttpSender = tokio::sync::oneshot::Sender; -type HttpResponseSenders = Arc>>; -type PathBindings = Arc>>; - -// node -> ID -> random ID - -/// http driver -pub async fn http_server( - our_name: String, - our_port: u16, - jwt_secret_bytes: Vec, - mut recv_in_server: MessageReceiver, - send_to_loop: MessageSender, - print_tx: PrintSender, -) -> Result<()> { - let http_response_senders = Arc::new(Mutex::new(HashMap::new())); - let websockets: WebSockets = Arc::new(Mutex::new(HashMap::new())); - let ws_proxies: WebSocketProxies = Arc::new(Mutex::new(HashMap::new())); // channel_id -> node - - // Add RPC path - let mut bindings_map: Router = Router::new(); - let rpc_bound_path = BoundPath { - app: ProcessId::from_str("rpc:sys:uqbar").unwrap(), - authenticated: false, - local_only: true, - original_path: "/rpc:sys:uqbar/message".to_string(), - }; - bindings_map.add("/rpc:sys:uqbar/message", rpc_bound_path); - - // Add encryptor binding - let encryptor_bound_path = BoundPath { - app: ProcessId::from_str("encryptor:sys:uqbar").unwrap(), - authenticated: false, - local_only: true, - original_path: "/encryptor:sys:uqbar".to_string(), - }; - bindings_map.add("/encryptor:sys:uqbar", encryptor_bound_path); - - let path_bindings: PathBindings = Arc::new(RwLock::new(bindings_map)); - - let _ = tokio::join!( - http_serve( - our_name.clone(), - our_port, - http_response_senders.clone(), - path_bindings.clone(), - websockets.clone(), - jwt_secret_bytes.clone(), - send_to_loop.clone(), - print_tx.clone() - ), - async move { - while let Some(kernel_message) = recv_in_server.recv().await { - let KernelMessage { - id, - source, - message, - payload, - .. - } = kernel_message; - - if let Err(e) = http_handle_messages( - our_name.clone(), - id, - source.clone(), - message, - payload, - http_response_senders.clone(), - path_bindings.clone(), - websockets.clone(), - ws_proxies.clone(), - jwt_secret_bytes.clone(), - send_to_loop.clone(), - print_tx.clone(), - ) - .await - { - send_to_loop - .send(make_error_message(our_name.clone(), id, source.clone(), e)) - .await - .unwrap(); - } - } - } - ); - Err(anyhow::anyhow!("http_server: exited")) -} - -async fn handle_websocket( - ws: WebSocket, - our: String, - jwt_secret_bytes: Vec, - websockets: WebSockets, - send_to_loop: MessageSender, - print_tx: PrintSender, -) { - let (write_stream, mut read_stream) = ws.split(); - let write_stream = Arc::new(Mutex::new(write_stream)); - - // How do we handle authentication? - let ws_id: u64 = rand::random(); - - while let Some(Ok(msg)) = read_stream.next().await { - if msg.is_binary() { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "GOT WEBSOCKET BYTES".to_string(), - }) - .await; - let bytes = msg.as_bytes(); - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!( - "WEBSOCKET MESSAGE (BYTES) {}", - String::from_utf8(bytes.to_vec()).unwrap_or_default() - ), - }) - .await; - match serde_json::from_slice::(bytes) { - Ok(parsed_msg) => { - handle_incoming_ws( - parsed_msg, - our.clone(), - jwt_secret_bytes.clone().to_vec(), - websockets.clone(), - send_to_loop.clone(), - print_tx.clone(), - write_stream.clone(), - ws_id, - ) - .await; - } - Err(e) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!("Failed to parse WebSocket message: {}", e), - }) - .await; - } - } - } else if msg.is_text() { - if let Ok(msg_str) = msg.to_str() { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!("WEBSOCKET MESSAGE (TEXT): {}", msg_str), - }) - .await; - if let Ok(parsed_msg) = serde_json::from_str(msg_str) { - handle_incoming_ws( - parsed_msg, - our.clone(), - jwt_secret_bytes.clone().to_vec(), - websockets.clone(), - send_to_loop.clone(), - print_tx.clone(), - write_stream.clone(), - ws_id, - ) - .await; - } - } - } else if msg.is_close() { - // Delete the websocket from the map - let mut ws_map = websockets.lock().await; - for (node, node_map) in ws_map.iter_mut() { - for (channel_id, id_map) in node_map.iter_mut() { - if id_map.remove(&ws_id).is_some() { - // Send disconnect message - send_ws_disconnect( - node.clone(), - our.clone(), - channel_id.clone(), - send_to_loop.clone(), - print_tx.clone(), - ) - .await; - } - } - } - } - } -} - -async fn http_handle_messages( - our: String, - id: u64, - source: Address, - message: Message, - payload: Option, - http_response_senders: HttpResponseSenders, - path_bindings: PathBindings, - websockets: WebSockets, - ws_proxies: WebSocketProxies, - jwt_secret_bytes: Vec, - send_to_loop: MessageSender, - print_tx: PrintSender, -) -> Result<(), HttpServerError> { - match message { - Message::Response((ref response, _)) => { - let mut senders = http_response_senders.lock().await; - match senders.remove(&id) { - // if no corresponding entry, nowhere to send response - None => {} - Some((path, channel)) => { - // if path is /rpc/message, return accordingly with base64 encoded payload - if path == *"/rpc:sys:uqbar/message" { - let payload = payload.map(|p| { - let bytes = p.bytes; - let base64_bytes = base64::encode(bytes); - Payload { - mime: p.mime, - bytes: base64_bytes.into_bytes(), - } - }); - let body = serde_json::json!({ - "ipc": response.ipc, - "payload": payload - }) - .to_string() - .as_bytes() - .to_vec(); - let mut default_headers = HashMap::new(); - default_headers.insert("Content-Type".to_string(), "text/html".to_string()); - - let _ = channel.send(HttpResponse { - status: 200, - headers: default_headers, - body: Some(body), - }); - // error case here? - } else { - // else try deserializing ipc into a HttpResponse - let json = serde_json::from_slice::(&response.ipc); - match json { - Ok(mut response) => { - let Some(payload) = payload else { - return Err(HttpServerError::NoBytes); - }; - let bytes = payload.bytes; - - // for the login case, todo refactor out? - let segments: Vec<&str> = path - .split('/') - .filter(|&segment| !segment.is_empty()) - .collect(); - - // If we're getting back a /login from a proxy (or our own node), then we should generate a jwt from the secret + the name of the ship, and then attach it to a header - if response.status < 400 - && (segments.len() == 1 || segments.len() == 4) - && matches!(segments.last(), Some(&"login")) - { - if let Some(auth_cookie) = response.headers.get("set-cookie") { - let mut ws_auth_username = our.clone(); - - if segments.len() == 4 - && matches!(segments.first(), Some(&"http-proxy")) - && matches!(segments.get(1), Some(&"serve")) - { - if let Some(segment) = segments.get(2) { - ws_auth_username = segment.to_string(); - } - } - if let Some(token) = register::generate_jwt( - jwt_secret_bytes.to_vec().as_slice(), - ws_auth_username.clone(), - ) { - let auth_cookie_with_ws = format!( - "{}; uqbar-ws-auth_{}={};", - auth_cookie, - ws_auth_username.clone(), - token - ); - response.headers.insert( - "set-cookie".to_string(), - auth_cookie_with_ws, - ); - - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!( - "SET WS AUTH COOKIE WITH USERNAME: {}", - ws_auth_username - ), - }) - .await; - } - } - } - let _ = channel.send(HttpResponse { - status: response.status, - headers: response.headers, - body: Some(bytes), - }); - } - Err(_json_parsing_err) => { - let mut error_headers = HashMap::new(); - error_headers - .insert("Content-Type".to_string(), "text/html".to_string()); - - let _ = channel.send(HttpResponse { - status: 503, - headers: error_headers, - body: Some( - "Internal Server Error".to_string().as_bytes().to_vec(), - ), - }); - } - } - } - } - } - } - Message::Request(Request { ipc, .. }) => { - if let Ok(message) = serde_json::from_slice(&ipc) { - match message { - HttpServerMessage::BindPath { - path, - authenticated, - local_only, - } => { - let mut path_bindings = path_bindings.write().await; - let app = source.process.clone().to_string(); - - let mut path = path.clone(); - if app != "homepage:homepage:uqbar" { - path = if path.starts_with('/') { - format!("/{}{}", app, path) - } else { - format!("/{}/{}", app, path) - }; - } - // trim trailing "/" - path = normalize_path(&path); - - let bound_path = BoundPath { - app: source.process, - authenticated, - local_only, - original_path: path.clone(), - }; - - path_bindings.add(&path, bound_path); - } - HttpServerMessage::WebSocketPush(WebSocketPush { target, is_text }) => { - let Some(payload) = payload else { - return Err(HttpServerError::NoBytes); - }; - let bytes = payload.bytes; - - let mut ws_map = websockets.lock().await; - let send_text = is_text.unwrap_or(false); - let response_data = if send_text { - warp::ws::Message::text( - String::from_utf8(bytes.clone()).unwrap_or_default(), - ) - } else { - warp::ws::Message::binary(bytes.clone()) - }; - - // Send to the proxy, if registered - if let Some(channel_id) = target.id.clone() { - let locked_proxies = ws_proxies.lock().await; - - if let Some(proxy_nodes) = locked_proxies.get(&channel_id) { - for proxy_node in proxy_nodes { - let id: u64 = rand::random(); - let bytes_content = bytes.clone(); - - // Send a message to the encryptor - let message = KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: Address { - node: proxy_node.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - rsvp: None, - message: Message::Request(Request { - inherit: false, - expects_response: None, - ipc: serde_json::json!({ // this is the JSON to forward - "WebSocketPush": { - "target": { - "node": our.clone(), // it's ultimately for us, but through the proxy - "id": channel_id.clone(), - }, - "is_text": send_text, - } - }).to_string().into_bytes(), - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: bytes_content, - }), - signed_capabilities: None, - }; - - send_to_loop.send(message).await.unwrap(); - } - } - } - - // Send to the websocket if registered - if let Some(node_map) = ws_map.get_mut(&target.node) { - if let Some(socket_id) = &target.id { - if let Some(ws_map) = node_map.get_mut(socket_id) { - // Iterate over ws_map values and send message to all websockets - for ws in ws_map.values_mut() { - let mut locked_write_stream = ws.lock().await; - let _ = - locked_write_stream.send(response_data.clone()).await; - // TODO: change this to binary - } - } else { - // Send to all websockets - for ws_map in node_map.values_mut() { - for ws in ws_map.values_mut() { - let mut locked_write_stream = ws.lock().await; - let _ = locked_write_stream - .send(response_data.clone()) - .await; - } - } - } - } else { - // Send to all websockets - for ws_map in node_map.values_mut() { - for ws in ws_map.values_mut() { - let mut locked_write_stream = ws.lock().await; - let _ = - locked_write_stream.send(response_data.clone()).await; - } - } - } - } else { - // Do nothing because we don't have a WS for that node - } - } - HttpServerMessage::ServerAction(ServerAction { action }) => { - if action == "get-jwt-secret" && source.node == our { - let id: u64 = rand::random(); - let message = KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: source, - rsvp: Some(Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }), - message: Message::Request(Request { - inherit: false, - expects_response: None, - ipc: serde_json::json!({ - "action": "set-jwt-secret" - }) - .to_string() - .into_bytes(), - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), // TODO adjust MIME type as needed - bytes: jwt_secret_bytes.clone(), - }), - signed_capabilities: None, - }; - - send_to_loop.send(message).await.unwrap(); - } - } - HttpServerMessage::WsRegister(WsRegister { - auth_token, - ws_auth_token: _, - channel_id, - }) => { - if let Ok(_node) = - parse_auth_token(auth_token, jwt_secret_bytes.clone().to_vec()) - { - add_ws_proxy(ws_proxies.clone(), channel_id, source.node.clone()).await; - } - } - HttpServerMessage::WsProxyDisconnect(WsProxyDisconnect { channel_id }) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "WsDisconnect".to_string(), - }) - .await; - // Check the ws_proxies for this channel_id, if it exists, delete the node that forwarded - let mut locked_proxies = ws_proxies.lock().await; - if let Some(proxy_nodes) = locked_proxies.get_mut(&channel_id) { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "disconnected".to_string(), - }) - .await; - proxy_nodes.remove(&source.node); - } - } - HttpServerMessage::WsMessage(WsMessage { - auth_token, - ws_auth_token: _, - channel_id, - target, - json, - }) => { - if let Ok(_node) = - parse_auth_token(auth_token, jwt_secret_bytes.clone().to_vec()) - { - add_ws_proxy(ws_proxies.clone(), channel_id, source.node.clone()).await; - - handle_ws_message( - target.clone(), - json.clone(), - our.clone(), - send_to_loop.clone(), - print_tx.clone(), - ) - .await; - } - } - HttpServerMessage::EncryptedWsMessage(EncryptedWsMessage { - auth_token, - ws_auth_token: _, - channel_id, - target, - encrypted, - nonce, - }) => { - if let Ok(_node) = - parse_auth_token(auth_token, jwt_secret_bytes.clone().to_vec()) - { - add_ws_proxy( - ws_proxies.clone(), - channel_id.clone(), - source.node.clone(), - ) - .await; - - handle_encrypted_ws_message( - target.clone(), - our.clone(), - channel_id.clone(), - encrypted.clone(), - nonce.clone(), - send_to_loop.clone(), - print_tx.clone(), - ) - .await; - } - } - } - } - } - } - - Ok(()) -} - -// TODO: add a way to register a websocket connection (should be a Vector of websockets) -// Then forward websocket messages to the correct place -async fn http_serve( - our: String, - our_port: u16, - http_response_senders: HttpResponseSenders, - path_bindings: PathBindings, - websockets: WebSockets, - jwt_secret_bytes: Vec, - send_to_loop: MessageSender, - print_tx: PrintSender, -) { - let cloned_msg_tx = send_to_loop.clone(); - let cloned_print_tx = print_tx.clone(); - let cloned_our = our.clone(); - let cloned_jwt_secret_bytes = jwt_secret_bytes.clone(); - let ws_route = warp::path::end() - .and(warp::ws()) - .and(warp::any().map(move || cloned_our.clone())) - .and(warp::any().map(move || cloned_jwt_secret_bytes.clone())) - .and(warp::any().map(move || websockets.clone())) - .and(warp::any().map(move || cloned_msg_tx.clone())) - .and(warp::any().map(move || cloned_print_tx.clone())) - .map( - |ws_connection: Ws, - our: String, - jwt_secret_bytes: Vec, - websockets: WebSockets, - send_to_loop: MessageSender, - print_tx: PrintSender| { - ws_connection.on_upgrade(move |ws: WebSocket| async move { - handle_websocket( - ws, - our, - jwt_secret_bytes, - websockets, - send_to_loop, - print_tx, - ) - .await - }) - }, - ); - - let print_tx_move = print_tx.clone(); - let filter = warp::filters::method::method() - .and(warp::addr::remote()) - .and(warp::path::full()) - .and(warp::filters::header::headers_cloned()) - .and( - warp::filters::query::raw() - .or(warp::any().map(String::default)) - .unify() - .map(|query_string: String| { - if query_string.is_empty() { - HashMap::new() - } else { - match serde_urlencoded::from_str(&query_string) { - Ok(map) => map, - Err(_) => HashMap::new(), - } - } - }), - ) - .and(warp::filters::body::bytes()) - .and(warp::any().map(move || our.clone())) - .and(warp::any().map(move || http_response_senders.clone())) - .and(warp::any().map(move || path_bindings.clone())) - .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_move.clone())) - .and_then(handler); - - let filter_with_ws = ws_route.or(filter); - - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!("http_server: running on: {}", our_port), - }) - .await; - warp::serve(filter_with_ws) - .run(([0, 0, 0, 0], our_port)) - .await; -} - -async fn handler( - method: warp::http::Method, - address: Option, - path: warp::path::FullPath, - headers: warp::http::HeaderMap, - query_params: HashMap, - body: warp::hyper::body::Bytes, - our: String, - http_response_senders: HttpResponseSenders, - path_bindings: PathBindings, - jwt_secret_bytes: Vec, - send_to_loop: MessageSender, - _print_tx: PrintSender, -) -> Result { - let address = match address { - Some(a) => a.to_string(), - None => "".to_string(), - }; - // trim trailing "/" - let original_path = normalize_path(path.as_str()); - let id: u64 = rand::random(); - let real_headers = serialize_headers(&headers); - let path_bindings = path_bindings.read().await; - - let Ok(route) = path_bindings.recognize(&original_path) else { - return Ok(warp::reply::with_status(vec![], StatusCode::NOT_FOUND).into_response()); - }; - let bound_path = route.handler(); - - let app = bound_path.app.to_string(); - let url_params: HashMap<&str, &str> = route.params().into_iter().collect(); - let raw_path = remove_process_id(&original_path); - let path = remove_process_id(&bound_path.original_path); - - if bound_path.authenticated { - let auth_token = real_headers.get("cookie").cloned().unwrap_or_default(); - if !auth_cookie_valid(our.clone(), &auth_token, jwt_secret_bytes) { - // send 401 - return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response()); - } - } - - if bound_path.local_only && !address.starts_with("127.0.0.1:") { - // send 403 - return Ok(warp::reply::with_status(vec![], StatusCode::FORBIDDEN).into_response()); - } - - // RPC functionality: if path is /rpc:sys:uqbar/message, - // we extract message from base64 encoded bytes in data - // and send it to the correct app. - - let message = if app == *"rpc:sys:uqbar" { - let rpc_message: RpcMessage = match serde_json::from_slice(&body) { - // to_vec()? - Ok(v) => v, - Err(_) => { - return Ok( - warp::reply::with_status(vec![], StatusCode::BAD_REQUEST).into_response() - ); - } - }; - - let target_process = match ProcessId::from_str(&rpc_message.process) { - Ok(p) => p, - Err(_) => { - return Ok( - warp::reply::with_status(vec![], StatusCode::BAD_REQUEST).into_response() - ); - } - }; - - let payload = match base64::decode(rpc_message.data.unwrap_or("".to_string())) { - Ok(bytes) => Some(Payload { - mime: rpc_message.mime, - bytes, - }), - Err(_) => None, - }; - let node = match rpc_message.node { - Some(node_str) => node_str, - None => our.clone(), - }; - - let ipc_bytes: Vec = match rpc_message.ipc { - Some(ipc_string) => ipc_string.into_bytes(), - None => Vec::new(), - }; - - KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: Address { - node, - process: target_process, - }, - rsvp: Some(Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }), - message: Message::Request(Request { - inherit: false, - expects_response: Some(15), // no effect on runtime - ipc: ipc_bytes, - metadata: rpc_message.metadata, - }), - payload, - signed_capabilities: None, - } - } else if app == *"encryptor:sys:uqbar" { - let body_json = match String::from_utf8(body.to_vec()) { - Ok(s) => s, - Err(_) => { - return Ok( - warp::reply::with_status(vec![], StatusCode::BAD_REQUEST).into_response() - ); - } - }; - - let body: serde_json::Value = match serde_json::from_str(&body_json) { - Ok(v) => v, - Err(_) => { - return Ok( - warp::reply::with_status(vec![], StatusCode::BAD_REQUEST).into_response() - ); - } - }; - - let channel_id = body["channel_id"].as_str().unwrap_or(""); - let public_key_hex = body["public_key_hex"].as_str().unwrap_or(""); - - KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: Address { - node: our.clone(), - process: ProcessId::from_str("encryptor:sys:uqbar").unwrap(), - }, - rsvp: None, //? - message: Message::Request(Request { - inherit: false, - expects_response: None, - ipc: serde_json::json!(EncryptorMessage::GetKey(GetKeyAction { - channel_id: channel_id.to_string(), - public_key_hex: public_key_hex.to_string(), - })) - .to_string() - .into_bytes(), - metadata: None, - }), - payload: None, - signed_capabilities: None, - } - } else { - // otherwise, make a message, to the correct app. - KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: Address { - node: our.clone(), - process: bound_path.app.clone(), - }, - rsvp: Some(Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }), - message: Message::Request(Request { - inherit: false, - expects_response: Some(15), // no effect on runtime - ipc: serde_json::json!({ - "address": address, - "method": method.to_string(), - "raw_path": raw_path.clone(), - "path": path.clone(), - "headers": serialize_headers(&headers), - "query_params": query_params, - "url_params": url_params, - }) - .to_string() - .into_bytes(), - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), // TODO adjust MIME type as needed - bytes: body.to_vec(), - }), - signed_capabilities: None, - } - }; - let (response_sender, response_receiver) = oneshot::channel(); - http_response_senders - .lock() - .await - .insert(id, (original_path.clone(), response_sender)); - - send_to_loop.send(message).await.unwrap(); - let timeout_duration = tokio::time::Duration::from_secs(15); // adjust as needed - let result = tokio::time::timeout(timeout_duration, response_receiver).await; - - let from_channel = match result { - Ok(Ok(from_channel)) => from_channel, - Ok(Err(_)) => { - return Ok( - warp::reply::with_status(vec![], StatusCode::INTERNAL_SERVER_ERROR).into_response(), - ); - } - Err(_) => { - return Ok( - warp::reply::with_status(vec![], StatusCode::REQUEST_TIMEOUT).into_response(), - ); - } - }; - - let reply = warp::reply::with_status( - match from_channel.body { - Some(val) => val, - None => vec![], - }, - StatusCode::from_u16(from_channel.status).unwrap(), - ); - let mut response = reply.into_response(); - - // Merge the deserialized headers into the existing headers - let existing_headers = response.headers_mut(); - for (header_name, header_value) in deserialize_headers(from_channel.headers).iter() { - if header_name == "set-cookie" || header_name == "Set-Cookie" { - if let Ok(cookie) = header_value.to_str() { - let cookie_headers: Vec<&str> = cookie - .split("; ") - .filter(|&cookie| !cookie.is_empty()) - .collect(); - for cookie_header in cookie_headers { - if let Ok(valid_cookie) = HeaderValue::from_str(cookie_header) { - existing_headers.append(header_name, valid_cookie); - } - } - } - } else { - existing_headers.insert(header_name.clone(), header_value.clone()); - } - } - Ok(response) -} - -pub async fn find_open_port(start_at: u16) -> Option { - for port in start_at..=u16::MAX { - let bind_addr = format!("0.0.0.0:{}", port); - if is_port_available(&bind_addr).await { - return Some(port); - } - } - None -} diff --git a/src/http_server/server_fns.rs b/src/http_server/server_fns.rs deleted file mode 100644 index a76054a7..00000000 --- a/src/http_server/server_fns.rs +++ /dev/null @@ -1,538 +0,0 @@ -use crate::types::*; -use futures::stream::SplitSink; -use hmac::{Hmac, Mac}; -use jwt::{Error, VerifyWithKey}; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use tokio::net::TcpListener; -use tokio::sync::Mutex; -use warp::http::{header::HeaderName, header::HeaderValue, HeaderMap}; -use warp::ws::WebSocket; - -pub type SharedWriteStream = Arc>>; -pub type WebSockets = Arc>>>>; -pub type WebSocketProxies = Arc>>>; - -pub struct BoundPath { - pub app: ProcessId, - pub authenticated: bool, - pub local_only: bool, - pub original_path: String, -} - -#[derive(Serialize, Deserialize)] -pub struct RpcMessage { - pub node: Option, - pub process: String, - pub inherit: Option, - pub expects_response: Option, - pub ipc: Option, - pub metadata: Option, - pub context: Option, - pub mime: Option, - pub data: Option, -} - -pub fn parse_auth_token(auth_token: String, jwt_secret: Vec) -> Result { - let secret: Hmac = match Hmac::new_from_slice(jwt_secret.as_slice()) { - Ok(secret) => secret, - Err(_) => { - return Ok("Error recovering jwt secret".to_string()); - } - }; - - let claims: Result = auth_token.verify_with_key(&secret); - - match claims { - Ok(data) => Ok(data.username), - Err(err) => Err(err), - } -} - -pub fn auth_cookie_valid(our_node: String, cookie: &str, jwt_secret: Vec) -> bool { - let cookie_parts: Vec<&str> = cookie.split("; ").collect(); - let mut auth_token = None; - - 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!("uqbar-auth_{}", our_node) - { - auth_token = Some(cookie_part_parts[1].to_string()); - break; - } - } - - let auth_token = match auth_token { - Some(token) if !token.is_empty() => token, - _ => return false, - }; - - let secret = match Hmac::::new_from_slice(&jwt_secret) { - Ok(secret) => secret, - Err(_) => return false, - }; - - let claims: Result = auth_token.verify_with_key(&secret); - - match claims { - Ok(data) => data.username == our_node, - Err(_) => false, - } -} - -pub fn remove_process_id(path: &str) -> String { - // Split the string into parts separated by '/' - let mut parts = path.splitn(3, '/'); - // Skip the first two parts (before and after the first '/') - let remaining_path = parts.nth(2).unwrap_or(""); - // If the result is empty, return "/" - if remaining_path.is_empty() { - return "/".to_string(); - } - // Otherwise, return the result with a leading "/" - format!("/{}", remaining_path) -} - -pub fn normalize_path(path: &str) -> String { - let mut normalized = path.to_string(); - if normalized != "/" && normalized.ends_with('/') { - normalized.pop(); - } - normalized -} - -pub async fn handle_incoming_ws( - parsed_msg: WebSocketClientMessage, - our: String, - jwt_secret_bytes: Vec, - websockets: WebSockets, - send_to_loop: MessageSender, - print_tx: PrintSender, - write_stream: SharedWriteStream, - ws_id: u64, -) { - let cloned_parsed_msg = parsed_msg.clone(); - match parsed_msg { - WebSocketClientMessage::WsRegister(WsRegister { - ws_auth_token, - auth_token: _, - channel_id, - }) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!("REGISTER: {} {}", ws_auth_token, channel_id), - }) - .await; - // Get node from auth token - if let Ok(node) = parse_auth_token(ws_auth_token, jwt_secret_bytes.clone()) { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!("NODE: {}", node), - }) - .await; - handle_ws_register( - node, - cloned_parsed_msg, - channel_id.clone(), - our.clone(), - websockets.clone(), - send_to_loop.clone(), - print_tx.clone(), - write_stream.clone(), - ws_id, - ) - .await; - } else { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "Auth token parsing failed for WsRegister".to_string(), - }) - .await; - } - } - // Forward to target's http_server with the auth_token - WebSocketClientMessage::WsMessage(WsMessage { - ws_auth_token, - auth_token: _, - target, - json, - .. - }) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!("ACTION: {}", target.node.clone()), - }) - .await; - // TODO: restrict sending actions to ourself and nodes for which we are proxying - // TODO: use the channel_id - if let Ok(node) = parse_auth_token(ws_auth_token, jwt_secret_bytes.clone()) { - if node == target.node { - if target.node == our { - handle_ws_message( - target.clone(), - json.clone(), - our.clone(), - send_to_loop.clone(), - print_tx.clone(), - ) - .await; - } else { - proxy_ws_message( - node, - cloned_parsed_msg, - our.clone(), - send_to_loop.clone(), - print_tx.clone(), - ) - .await; - } - } - } - } - // Forward to target's http_server with the auth_token - WebSocketClientMessage::EncryptedWsMessage(EncryptedWsMessage { - ws_auth_token, - auth_token: _, - channel_id, - target, - encrypted, - nonce, - }) => { - let _ = print_tx - .send(Printout { - verbosity: 1, - content: format!("ENCRYPTED ACTION: {}", target.node.clone()), - }) - .await; - if let Ok(node) = parse_auth_token(ws_auth_token, jwt_secret_bytes.clone()) { - if node == target.node { - if target.node == our { - handle_encrypted_ws_message( - target.clone(), - our.clone(), - channel_id.clone(), - encrypted.clone(), - nonce.clone(), - send_to_loop.clone(), - print_tx.clone(), - ) - .await; - } else { - proxy_ws_message( - node, - cloned_parsed_msg, - our.clone(), - send_to_loop.clone(), - print_tx.clone(), - ) - .await; - } - } - } - } - } -} - -pub fn serialize_headers(headers: &HeaderMap) -> HashMap { - let mut hashmap = HashMap::new(); - for (key, value) in headers.iter() { - let key_str = key.to_string(); - let value_str = value.to_str().unwrap_or("").to_string(); - hashmap.insert(key_str, value_str); - } - hashmap -} - -pub fn deserialize_headers(hashmap: HashMap) -> HeaderMap { - let mut header_map = HeaderMap::new(); - for (key, value) in hashmap { - let key_bytes = key.as_bytes(); - let key_name = HeaderName::from_bytes(key_bytes).unwrap(); - let value_header = HeaderValue::from_str(&value).unwrap(); - header_map.insert(key_name, value_header); - } - header_map -} - -pub async fn is_port_available(bind_addr: &str) -> bool { - TcpListener::bind(bind_addr).await.is_ok() -} - -pub fn binary_encoded_string_to_bytes(s: &str) -> Vec { - s.chars().map(|c| c as u8).collect() -} - -pub async fn handle_ws_register( - node: String, - parsed_msg: WebSocketClientMessage, - channel_id: String, - our: String, - websockets: WebSockets, - send_to_loop: MessageSender, - print_tx: PrintSender, - write_stream: SharedWriteStream, - ws_id: u64, -) { - // let _ = print_tx.send(Printout { verbosity: 1, content: format!("1.2 {}", node) }).await; - // TODO: restrict registration to ourself and nodes for which we are proxying - let mut ws_map = websockets.lock().await; - let node_map = ws_map.entry(node.clone()).or_insert(HashMap::new()); - let id_map = node_map.entry(channel_id.clone()).or_insert(HashMap::new()); - id_map.insert(ws_id, write_stream.clone()); - - // Send a message to the target node to add to let it know we are proxying - if node != our { - let id: u64 = rand::random(); - let message = KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: Address { - node: node.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - rsvp: None, - message: Message::Request(Request { - inherit: false, - expects_response: None, - ipc: serde_json::json!(parsed_msg).to_string().into_bytes(), - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: vec![], - }), - signed_capabilities: None, - }; - - send_to_loop.send(message).await.unwrap(); - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "WEBSOCKET CHANNEL FORWARDED!".to_string(), - }) - .await; - } - - let _ = print_tx - .send(Printout { - verbosity: 1, - content: "WEBSOCKET CHANNEL REGISTERED!".to_string(), - }) - .await; -} - -pub async fn handle_ws_message( - target: Address, - json: Option, - our: String, - send_to_loop: MessageSender, - _print_tx: PrintSender, -) { - let id: u64 = rand::random(); - let message = KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: target.clone(), - rsvp: None, - message: Message::Request(Request { - inherit: false, - expects_response: None, - ipc: vec![], - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: json.unwrap_or_default().to_string().as_bytes().to_vec(), - }), - signed_capabilities: None, - }; - - send_to_loop.send(message).await.unwrap(); -} - -pub async fn handle_encrypted_ws_message( - target: Address, - our: String, - channel_id: String, - encrypted: String, - nonce: String, - send_to_loop: MessageSender, - _print_tx: PrintSender, -) { - let encrypted_bytes = binary_encoded_string_to_bytes(&encrypted); - let nonce_bytes = binary_encoded_string_to_bytes(&nonce); - - let mut encrypted_data = encrypted_bytes; - encrypted_data.extend(nonce_bytes); - - let id: u64 = rand::random(); - - // Send a message to the encryptor - let message = KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: Address { - node: target.node.clone(), - process: ENCRYPTOR_PROCESS_ID.clone(), - }, - rsvp: None, - message: Message::Request(Request { - inherit: false, - expects_response: None, - ipc: serde_json::json!(EncryptorMessage::DecryptAndForward( - DecryptAndForwardAction { - channel_id: channel_id.clone(), - forward_to: target.clone(), - json: Some(serde_json::json!({ - "forwarded_from": { - "node": our.clone(), - "process": "http_server:sys:uqbar", - } - })), - } - )) - .to_string() - .into_bytes(), - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: encrypted_data, - }), - signed_capabilities: None, - }; - - send_to_loop.send(message).await.unwrap(); -} - -pub async fn proxy_ws_message( - node: String, - parsed_msg: WebSocketClientMessage, - our: String, - send_to_loop: MessageSender, - _print_tx: PrintSender, -) { - let id: u64 = rand::random(); - let message = KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: Address { - node, - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - rsvp: None, - message: Message::Request(Request { - inherit: false, - expects_response: None, - ipc: serde_json::json!(parsed_msg).to_string().into_bytes(), - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: vec![], - }), - signed_capabilities: None, - }; - - send_to_loop.send(message).await.unwrap(); -} - -pub async fn add_ws_proxy(ws_proxies: WebSocketProxies, channel_id: String, source_node: String) { - let mut locked_proxies = ws_proxies.lock().await; - if let Some(proxy_nodes) = locked_proxies.get_mut(&channel_id) { - if !proxy_nodes.contains(&source_node) { - proxy_nodes.insert(source_node); - } - } else { - let mut proxy_nodes = HashSet::new(); - proxy_nodes.insert(source_node); - locked_proxies.insert(channel_id, proxy_nodes); - } -} - -pub async fn send_ws_disconnect( - node: String, - our: String, - channel_id: String, - send_to_loop: MessageSender, - _print_tx: PrintSender, -) { - let id: u64 = rand::random(); - let message = KernelMessage { - id, - source: Address { - node: our.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target: Address { - node: node.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - rsvp: None, - message: Message::Request(Request { - inherit: false, - expects_response: None, - ipc: serde_json::json!({ - "WsProxyDisconnect": { - "channel_id": channel_id.clone(), - } - }) - .to_string() - .into_bytes(), - metadata: None, - }), - payload: Some(Payload { - mime: Some("application/octet-stream".to_string()), - bytes: vec![], - }), - signed_capabilities: None, - }; - - send_to_loop.send(message).await.unwrap(); -} - -pub fn make_error_message( - our_name: String, - id: u64, - target: Address, - error: HttpServerError, -) -> KernelMessage { - KernelMessage { - id, - source: Address { - node: our_name.clone(), - process: HTTP_SERVER_PROCESS_ID.clone(), - }, - target, - rsvp: None, - message: Message::Response(( - Response { - inherit: false, - ipc: serde_json::to_vec(&error).unwrap(), - metadata: None, - }, - None, - )), - payload: None, - signed_capabilities: None, - } -} diff --git a/src/kernel/mod.rs b/src/kernel/mod.rs index 038dec67..c26fe59a 100644 --- a/src/kernel/mod.rs +++ b/src/kernel/mod.rs @@ -2169,7 +2169,7 @@ async fn make_event_loop( let _ = persist_state(&our_name, &send_to_loop, &process_map).await; let _ = responder.send(true); }, - t::CapMessage::Drop { on, cap, responder } => { + t::CapMessage::_Drop { on, cap, responder } => { // remove cap from process map let Some(entry) = process_map.get_mut(&on) else { let _ = responder.send(false); diff --git a/src/kernel_types.rs b/src/kernel_types.rs deleted file mode 100644 index c88ef069..00000000 --- a/src/kernel_types.rs +++ /dev/null @@ -1,422 +0,0 @@ -use super::bindings::component::uq_process::types as wit; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; - -// -// process-facing kernel types, used for process -// management and message-passing -// matches types in uqbar.wit -// - -pub type Context = Vec; -pub type NodeId = String; // QNS domain name - -/// process ID is a formatted unique identifier that contains -/// the publishing node's ID, the package name, and finally the process name. -/// the process name can be a random number, or a name chosen by the user. -/// the formatting is as follows: -/// `[process name]:[package name]:[node ID]` -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct ProcessId { - process_name: String, - package_name: String, - publisher_node: NodeId, -} - -#[allow(dead_code)] -impl ProcessId { - /// generates a random u64 number if process_name is not declared - pub fn new(process_name: &str, package_name: &str, publisher_node: &str) -> Self { - ProcessId { - process_name: process_name.into(), - package_name: package_name.into(), - publisher_node: publisher_node.into(), - } - } - pub fn from_str(input: &str) -> Result { - // split string on colons into 3 segments - let mut segments = input.split(':'); - let process_name = segments - .next() - .ok_or(ProcessIdParseError::MissingField)? - .to_string(); - let package_name = segments - .next() - .ok_or(ProcessIdParseError::MissingField)? - .to_string(); - let publisher_node = segments - .next() - .ok_or(ProcessIdParseError::MissingField)? - .to_string(); - if segments.next().is_some() { - return Err(ProcessIdParseError::TooManyColons); - } - Ok(ProcessId { - process_name, - package_name, - publisher_node, - }) - } - pub fn to_string(&self) -> String { - [ - self.process_name.as_str(), - self.package_name.as_str(), - self.publisher_node.as_str(), - ] - .join(":") - } - pub fn process(&self) -> &str { - &self.process_name - } - pub fn package(&self) -> &str { - &self.package_name - } - pub fn publisher_node(&self) -> &str { - &self.publisher_node - } - pub fn en_wit(&self) -> wit::ProcessId { - wit::ProcessId { - process_name: self.process_name.clone(), - package_name: self.package_name.clone(), - publisher_node: self.publisher_node.clone(), - } - } - pub fn de_wit(wit: wit::ProcessId) -> ProcessId { - ProcessId { - process_name: wit.process_name, - package_name: wit.package_name, - publisher_node: wit.publisher_node, - } - } -} - -#[derive(Debug)] -pub enum ProcessIdParseError { - TooManyColons, - MissingField, -} - -#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] -pub struct Address { - pub node: NodeId, - pub process: ProcessId, -} - -impl Address { - pub fn en_wit(&self) -> wit::Address { - wit::Address { - node: self.node.clone(), - process: self.process.en_wit(), - } - } - pub fn de_wit(wit: wit::Address) -> Address { - Address { - node: wit.node, - process: ProcessId { - process_name: wit.process.process_name, - package_name: wit.process.package_name, - publisher_node: wit.process.publisher_node, - }, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Payload { - pub mime: Option, // MIME type - pub bytes: Vec, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Request { - pub inherit: bool, - pub expects_response: Option, // number of seconds until timeout - pub ipc: Vec, - pub metadata: Option, // JSON-string -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Response { - pub inherit: bool, - pub ipc: Vec, - pub metadata: Option, // JSON-string -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum Message { - Request(Request), - Response((Response, Option)), -} - -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct Capability { - pub issuer: Address, - pub params: String, // JSON-string -} - -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct SignedCapability { - pub issuer: Address, - pub params: String, // JSON-string - pub signature: Vec, // signed by the kernel, so we can verify that the kernel issued it -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SendError { - pub kind: SendErrorKind, - pub target: Address, - pub message: Message, - pub payload: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum SendErrorKind { - Offline, - Timeout, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum OnPanic { - None, - Restart, - Requests(Vec<(Address, Request, Option)>), -} - -impl OnPanic { - pub fn is_restart(&self) -> bool { - match self { - OnPanic::None => false, - OnPanic::Restart => true, - OnPanic::Requests(_) => false, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum KernelCommand { - StartProcess { - id: ProcessId, - wasm_bytes_handle: u128, - on_panic: OnPanic, - initial_capabilities: HashSet, - public: bool, - }, - KillProcess(ProcessId), // this is extrajudicial killing: we might lose messages! - // kernel only - RebootProcess { - process_id: ProcessId, - persisted: PersistedProcess, - }, - Shutdown, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum KernelResponse { - StartedProcess, - StartProcessError, - KilledProcess(ProcessId), -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PersistedProcess { - pub wasm_bytes_handle: u128, - // pub drive: String, - // pub full_path: String, - pub on_panic: OnPanic, - pub capabilities: HashSet, - pub public: bool, // marks if a process allows messages from any process -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct VfsRequest { - pub drive: String, - pub action: VfsAction, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum VfsAction { - New, - Add { - full_path: String, - entry_type: AddEntryType, - }, - Rename { - full_path: String, - new_full_path: String, - }, - Delete(String), - WriteOffset { - full_path: String, - offset: u64, - }, - SetSize { - full_path: String, - size: u64, - }, - GetPath(u128), - GetHash(String), - GetEntry(String), - GetFileChunk { - full_path: String, - offset: u64, - length: u64, - }, - GetEntryLength(String), -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum AddEntryType { - Dir, - NewFile, // add a new file to fs and add name in vfs - ExistingFile { hash: u128 }, // link an existing file in fs to a new name in vfs - ZipArchive, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum GetEntryType { - Dir, - File, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum VfsResponse { - Ok, - Err(VfsError), - GetPath(Option), - GetHash(Option), - GetEntry { - // file bytes in payload, if entry was a file - is_file: bool, - children: Vec, - }, - GetFileChunk, // chunk in payload - GetEntryLength(u64), -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum VfsError { - BadDriveName, - BadDescriptor, - NoCap, - EntryNotFound, -} - -#[allow(dead_code)] -impl VfsError { - pub fn kind(&self) -> &str { - match *self { - VfsError::BadDriveName => "BadDriveName", - VfsError::BadDescriptor => "BadDescriptor", - VfsError::NoCap => "NoCap", - VfsError::EntryNotFound => "EntryNotFound", - } - } -} - -// -// package types -// - -pub type PackageVersion = (u32, u32, u32); - -/// the type that gets deserialized from `metadata.json` in a package -#[derive(Debug, Serialize, Deserialize)] -pub struct PackageMetadata { - pub package: String, - pub publisher: String, - pub version: PackageVersion, - pub description: Option, - pub website: Option, -} - -/// the type that gets deserialized from each entry in the array in `manifest.json` -#[derive(Debug, Serialize, Deserialize)] -pub struct PackageManifestEntry { - pub process_name: String, - pub process_wasm_path: String, - pub on_panic: OnPanic, - pub request_networking: bool, - pub request_messaging: Vec, - pub public: bool, -} - -// -// conversions between wit types and kernel types (annoying!) -// - -pub fn de_wit_request(wit: wit::Request) -> Request { - Request { - inherit: wit.inherit, - expects_response: wit.expects_response, - ipc: wit.ipc, - metadata: wit.metadata, - } -} - -pub fn en_wit_request(request: Request) -> wit::Request { - wit::Request { - inherit: request.inherit, - expects_response: request.expects_response, - ipc: request.ipc, - metadata: request.metadata, - } -} - -pub fn de_wit_response(wit: wit::Response) -> Response { - Response { - inherit: wit.inherit, - ipc: wit.ipc, - metadata: wit.metadata, - } -} - -pub fn en_wit_response(response: Response) -> wit::Response { - wit::Response { - inherit: response.inherit, - ipc: response.ipc, - metadata: response.metadata, - } -} - -pub fn de_wit_payload(wit: Option) -> Option { - match wit { - None => None, - Some(wit) => Some(Payload { - mime: wit.mime, - bytes: wit.bytes, - }), - } -} - -pub fn en_wit_payload(load: Option) -> Option { - match load { - None => None, - Some(load) => Some(wit::Payload { - mime: load.mime, - bytes: load.bytes, - }), - } -} - -pub fn de_wit_signed_capability(wit: wit::SignedCapability) -> SignedCapability { - SignedCapability { - issuer: Address { - node: wit.issuer.node, - process: ProcessId { - process_name: wit.issuer.process.process_name, - package_name: wit.issuer.process.package_name, - publisher_node: wit.issuer.process.publisher_node, - }, - }, - params: wit.params, - signature: wit.signature, - } -} - -pub fn en_wit_signed_capability(cap: SignedCapability) -> wit::SignedCapability { - wit::SignedCapability { - issuer: cap.issuer.en_wit(), - params: cap.params, - signature: cap.signature, - } -} diff --git a/src/main.rs b/src/main.rs index c208e192..7da96193 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,9 @@ use std::sync::Arc; use tokio::sync::{mpsc, oneshot}; use tokio::{fs, time::timeout}; -mod encryptor; mod eth_rpc; mod filesystem; -mod http_client; -mod http_server; +mod http; mod kernel; mod keygen; mod net; @@ -35,7 +33,6 @@ const HTTP_CHANNEL_CAPACITY: usize = 32; const HTTP_CLIENT_CHANNEL_CAPACITY: usize = 32; const ETH_RPC_CHANNEL_CAPACITY: usize = 32; const VFS_CHANNEL_CAPACITY: usize = 1_000; -const ENCRYPTOR_CHANNEL_CAPACITY: usize = 32; const CAP_CHANNEL_CAPACITY: usize = 1_000; #[cfg(feature = "llm")] const LLM_CHANNEL_CAPACITY: usize = 32; @@ -104,9 +101,6 @@ async fn main() { // vfs maintains metadata about files in fs for processes let (vfs_message_sender, vfs_message_receiver): (MessageSender, MessageReceiver) = mpsc::channel(VFS_CHANNEL_CAPACITY); - // encryptor handles end-to-end encryption for client messages - let (encryptor_sender, encryptor_receiver): (MessageSender, MessageReceiver) = - mpsc::channel(ENCRYPTOR_CHANNEL_CAPACITY); // terminal receives prints via this channel, all other modules send prints let (print_sender, print_receiver): (PrintSender, PrintReceiver) = mpsc::channel(TERMINAL_CHANNEL_CAPACITY); @@ -204,7 +198,7 @@ async fn main() { // username, networking key, and routing info. // if any do not match, we should prompt user to create a "transaction" // that updates their PKI info on-chain. - let http_server_port = http_server::find_open_port(8080).await.unwrap(); + let http_server_port = http::utils::find_open_port(8080).await.unwrap(); println!("login or register at http://localhost:{}", http_server_port); let (kill_tx, kill_rx) = oneshot::channel::(); @@ -260,11 +254,6 @@ async fn main() { vfs_message_sender, true, ), - ( - ProcessId::new(Some("encryptor"), "sys", "uqbar"), - encryptor_sender, - false, - ), ]; #[cfg(feature = "llm")] @@ -334,7 +323,7 @@ async fn main() { fs_kill_recv, fs_kill_confirm_send, )); - tasks.spawn(http_server::http_server( + tasks.spawn(http::server::http_server( our.name.clone(), http_server_port, decoded_keyfile.jwt_secret_bytes.clone(), @@ -342,7 +331,7 @@ async fn main() { kernel_message_sender.clone(), print_sender.clone(), )); - tasks.spawn(http_client::http_client( + tasks.spawn(http::client::http_client( our.name.clone(), kernel_message_sender.clone(), http_client_receiver, @@ -369,13 +358,6 @@ async fn main() { caps_oracle_sender.clone(), vfs_messages, )); - tasks.spawn(encryptor::encryptor( - our.name.clone(), - networking_keypair_arc.clone(), - kernel_message_sender.clone(), - encryptor_receiver, - print_sender.clone(), - )); #[cfg(feature = "llm")] { tasks.spawn(llm::llm( diff --git a/src/register.rs b/src/register.rs index 98bef5a4..f91ed547 100644 --- a/src/register.rs +++ b/src/register.rs @@ -17,7 +17,6 @@ use warp::{ Filter, Rejection, Reply, }; -use crate::http_server; use crate::keygen; use crate::types::*; @@ -29,7 +28,7 @@ pub fn generate_jwt(jwt_secret_bytes: &[u8], username: String) -> Option Err(_) => return None, }; - let claims = JwtClaims { + let claims = crate::http::types::JwtClaims { username: username.clone(), expiration: 0, }; @@ -304,7 +303,7 @@ async fn handle_info( }; // TODO: if IP is localhost, don't allow registration as direct - let ws_port = http_server::find_open_port(9000).await.unwrap(); + let ws_port = crate::http::utils::find_open_port(9000).await.unwrap(); let our = Identity { networking_key: format!("0x{}", public_key), diff --git a/src/timer.rs b/src/timer.rs index cdd7cefd..2cf2728c 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -69,7 +69,7 @@ pub async fn timer_service( if !timer_map.contains(pop_time) { timer_tasks.spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(timer_millis - 1)).await; - return pop_time + pop_time }); } timer_map.insert(pop_time, km.id, km.rsvp.unwrap_or(km.source)); @@ -96,10 +96,7 @@ struct TimerMap { impl TimerMap { fn insert(&mut self, pop_time: u64, id: u64, addr: Address) { - self.timers - .entry(pop_time) - .or_insert(vec![]) - .push((id, addr)); + self.timers.entry(pop_time).or_default().push((id, addr)); } fn contains(&mut self, pop_time: u64) -> bool { diff --git a/src/types.rs b/src/types.rs index f12677b7..57e5fb39 100644 --- a/src/types.rs +++ b/src/types.rs @@ -18,10 +18,10 @@ lazy_static::lazy_static! { // // types shared between kernel and processes. frustratingly, this is an exact copy -// of the types in process_lib/src/kernel_types.rs +// of the types in process_lib // this is because even though the types are identical, they will not match when // used in the kernel context which generates bindings differently than the process -// standard library. make sure to keep this synced with kernel_types.rs +// standard library. make sure to keep this synced with process_lib. // pub type Context = Vec; pub type NodeId = String; // QNS domain name @@ -31,14 +31,63 @@ pub type NodeId = String; // QNS domain name /// the process name can be a random number, or a name chosen by the user. /// the formatting is as follows: /// `[process name]:[package name]:[node ID]` -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct ProcessId { process_name: String, package_name: String, publisher_node: NodeId, } -#[allow(dead_code)] +/// PackageId is like a ProcessId, but for a package. Only contains the name +/// of the package and the name of the publisher. +#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] +pub struct PackageId { + package_name: String, + publisher_node: String, +} + +impl PackageId { + pub fn _new(package_name: &str, publisher_node: &str) -> Self { + PackageId { + package_name: package_name.into(), + publisher_node: publisher_node.into(), + } + } + pub fn _from_str(input: &str) -> Result { + // split string on colons into 2 segments + let mut segments = input.split(':'); + let package_name = segments + .next() + .ok_or(ProcessIdParseError::MissingField)? + .to_string(); + let publisher_node = segments + .next() + .ok_or(ProcessIdParseError::MissingField)? + .to_string(); + if segments.next().is_some() { + return Err(ProcessIdParseError::TooManyColons); + } + Ok(PackageId { + package_name, + publisher_node, + }) + } + pub fn _package(&self) -> &str { + &self.package_name + } + pub fn _publisher(&self) -> &str { + &self.publisher_node + } +} + +impl std::fmt::Display for PackageId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.package_name, self.publisher_node) + } +} + +/// ProcessId is defined in the wit bindings, but constructors and methods +/// are defined here. impl ProcessId { /// generates a random u64 number if process_name is not declared pub fn new(process_name: Option<&str>, package_name: &str, publisher_node: &str) -> Self { @@ -99,12 +148,70 @@ impl ProcessId { } } +impl From<(&str, &str, &str)> for ProcessId { + fn from(input: (&str, &str, &str)) -> Self { + ProcessId::new(Some(input.0), input.1, input.2) + } +} + +impl std::fmt::Display for ProcessId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}:{}:{}", + self.process_name, self.package_name, self.publisher_node + ) + } +} + +// impl PartialEq for ProcessId { +// fn eq(&self, other: &Self) -> bool { +// self.process_name == other.process_name +// && self.package_name == other.package_name +// && self.publisher_node == other.publisher_node +// } +// } + +impl PartialEq<&str> for ProcessId { + fn eq(&self, other: &&str) -> bool { + &self.to_string() == other + } +} + +impl PartialEq for &str { + fn eq(&self, other: &ProcessId) -> bool { + self == &other.to_string() + } +} + #[derive(Debug)] pub enum ProcessIdParseError { TooManyColons, MissingField, } +impl std::fmt::Display for ProcessIdParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + ProcessIdParseError::TooManyColons => "Too many colons in ProcessId string", + ProcessIdParseError::MissingField => "Missing field in ProcessId string", + } + ) + } +} + +impl std::error::Error for ProcessIdParseError { + fn description(&self) -> &str { + match self { + ProcessIdParseError::TooManyColons => "Too many colons in ProcessId string", + ProcessIdParseError::MissingField => "Missing field in ProcessId string", + } + } +} + #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct Address { pub node: NodeId, @@ -112,6 +219,51 @@ pub struct Address { } impl Address { + pub fn new(node: &str, process: T) -> Address + where + T: Into, + { + Address { + node: node.to_string(), + process: process.into(), + } + } + pub fn _from_str(input: &str) -> Result { + // split string on colons into 4 segments, + // first one with @, next 3 with : + let mut name_rest = input.split('@'); + let node = name_rest + .next() + .ok_or(AddressParseError::MissingField)? + .to_string(); + let mut segments = name_rest + .next() + .ok_or(AddressParseError::MissingNodeId)? + .split(':'); + let process_name = segments + .next() + .ok_or(AddressParseError::MissingField)? + .to_string(); + let package_name = segments + .next() + .ok_or(AddressParseError::MissingField)? + .to_string(); + let publisher_node = segments + .next() + .ok_or(AddressParseError::MissingField)? + .to_string(); + if segments.next().is_some() { + return Err(AddressParseError::TooManyColons); + } + Ok(Address { + node, + process: ProcessId { + process_name, + package_name, + publisher_node, + }, + }) + } pub fn en_wit(&self) -> wit::Address { wit::Address { node: self.node.clone(), @@ -130,6 +282,59 @@ impl Address { } } +impl From<(&str, &str, &str, &str)> for Address { + fn from(input: (&str, &str, &str, &str)) -> Self { + Address::new(input.0, (input.1, input.2, input.3)) + } +} + +impl From<(&str, T)> for Address +where + T: Into, +{ + fn from(input: (&str, T)) -> Self { + Address::new(input.0, input.1) + } +} + +impl std::fmt::Display for Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.node, self.process) + } +} + +#[derive(Debug)] +#[allow(dead_code)] +pub enum AddressParseError { + TooManyColons, + MissingNodeId, + MissingField, +} + +impl std::fmt::Display for AddressParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + AddressParseError::TooManyColons => "Too many colons in ProcessId string", + AddressParseError::MissingNodeId => "Node ID missing", + AddressParseError::MissingField => "Missing field in ProcessId string", + } + ) + } +} + +impl std::error::Error for AddressParseError { + fn description(&self) -> &str { + match self { + AddressParseError::TooManyColons => "Too many colons in ProcessId string", + AddressParseError::MissingNodeId => "Node ID missing", + AddressParseError::MissingField => "Missing field in ProcessId string", + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Payload { pub mime: Option, // MIME type @@ -201,28 +406,6 @@ impl OnPanic { } } -// -// display impls -// - -impl std::fmt::Display for ProcessId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}:{}:{}", - self.process(), - self.package(), - self.publisher() - ) - } -} - -impl std::fmt::Display for Address { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}@{}", self.node, self.process) - } -} - impl std::fmt::Display for Message { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { @@ -382,7 +565,7 @@ pub fn de_wit_on_panic(wit: wit::OnPanic) -> OnPanic { } } // -// END SYNC WITH kernel_types.rs +// END SYNC WITH process_lib // // @@ -488,6 +671,24 @@ pub struct KernelMessage { pub signed_capabilities: Option>, } +impl std::fmt::Display for KernelMessage { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{{\n id: {},\n source: {},\n target: {},\n rsvp: {},\n message: {},\n payload: {}\n}}", + self.id, + self.source, + self.target, + match &self.rsvp { + Some(rsvp) => rsvp.to_string(), + None => "None".to_string() + }, + self.message, + self.payload.is_some(), + ) + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct WrappedSendError { pub id: u64, @@ -509,17 +710,6 @@ pub struct Printout { // -> kernel sets `Some(A) = Rsvp` for B's request to C pub type Rsvp = Option
    ; -// -// boot/startup specific types??? -// - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BootOutboundRequest { - pub target_process: ProcessId, - pub json: Option, - pub bytes: Option>, -} - #[derive(Debug, Serialize, Deserialize)] pub enum DebugCommand { Toggle, @@ -545,7 +735,6 @@ pub enum KernelCommand { Shutdown, } -#[allow(dead_code)] #[derive(Debug)] pub enum CapMessage { Add { @@ -553,7 +742,7 @@ pub enum CapMessage { cap: Capability, responder: tokio::sync::oneshot::Sender, }, - Drop { + _Drop { // not used yet! on: ProcessId, cap: Capability, @@ -598,14 +787,6 @@ pub struct ProcessContext { pub context: Option, } -// -// runtime-module-specific types -// - -// -// filesystem.rs types -// - pub type PackageVersion = (u32, u32, u32); /// the type that gets deserialized from `metadata.json` in a package @@ -833,232 +1014,3 @@ impl VfsError { } } } - -// -// http_client.rs types -// - -#[derive(Debug, Serialize, Deserialize)] -pub struct HttpClientRequest { - pub uri: String, - pub method: String, - pub headers: HashMap, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct HttpClientResponse { - pub status: u16, - pub headers: HashMap, -} - -#[derive(Error, Debug, Serialize, Deserialize)] -pub enum HttpClientError { - #[error("http_client: rsvp is None but message is expecting response")] - BadRsvp, - #[error("http_client: no json in request")] - NoJson, - #[error( - "http_client: JSON payload could not be parsed to HttpClientRequest: {error}. Got {:?}.", - json - )] - BadJson { json: String, error: String }, - #[error("http_client: http method not supported: {:?}", method)] - BadMethod { method: String }, - #[error("http_client: failed to execute request {:?}", error)] - RequestFailed { error: String }, -} - -#[allow(dead_code)] -impl HttpClientError { - pub fn kind(&self) -> &str { - match *self { - HttpClientError::BadRsvp { .. } => "BadRsvp", - HttpClientError::NoJson { .. } => "NoJson", - HttpClientError::BadJson { .. } => "BadJson", - HttpClientError::BadMethod { .. } => "BadMethod", - HttpClientError::RequestFailed { .. } => "RequestFailed", - } - } -} - -// -// custom kernel displays -// - -impl std::fmt::Display for KernelMessage { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{{\n id: {},\n source: {},\n target: {},\n rsvp: {},\n message: {},\n payload: {}\n}}", - self.id, - self.source, - self.target, - match &self.rsvp { - Some(rsvp) => rsvp.to_string(), - None => "None".to_string() - }, - self.message, - self.payload.is_some(), - ) - } -} - -// -// http_server.rs types -// - -#[derive(Debug, Serialize, Deserialize)] -pub struct HttpResponse { - pub status: u16, - pub headers: HashMap, - pub body: Option>, // TODO does this use a lot of memory? -} - -#[derive(Error, Debug, Serialize, Deserialize)] -pub enum HttpServerError { - #[error("http_server: json is None")] - NoJson, - #[error("http_server: response not ok")] - ResponseError, - #[error("http_server: bytes are None")] - NoBytes, - #[error( - "http_server: JSON payload could not be parsed to HttpClientRequest: {error}. Got {:?}.", - json - )] - BadJson { json: String, error: String }, - #[error("http_server: path binding error: {:?}", error)] - PathBind { error: String }, -} - -#[allow(dead_code)] -impl HttpServerError { - pub fn kind(&self) -> &str { - match *self { - HttpServerError::NoJson { .. } => "NoJson", - HttpServerError::NoBytes { .. } => "NoBytes", - HttpServerError::BadJson { .. } => "BadJson", - HttpServerError::ResponseError { .. } => "ResponseError", - HttpServerError::PathBind { .. } => "PathBind", - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct JwtClaims { - pub username: String, - pub expiration: u64, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WebSocketServerTarget { - pub node: String, - pub id: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct WebSocketPush { - pub target: WebSocketServerTarget, - pub is_text: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ServerAction { - pub action: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum HttpServerMessage { - BindPath { - path: String, - authenticated: bool, - local_only: bool, - }, - WebSocketPush(WebSocketPush), - ServerAction(ServerAction), - WsRegister(WsRegister), // Coming from a proxy - WsProxyDisconnect(WsProxyDisconnect), // Coming from a proxy - WsMessage(WsMessage), // Coming from a proxy - EncryptedWsMessage(EncryptedWsMessage), // Coming from a proxy -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WsRegister { - pub ws_auth_token: String, - pub auth_token: String, - pub channel_id: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WsProxyDisconnect { - // Doesn't require auth because it's coming from the proxy - pub channel_id: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WsMessage { - pub ws_auth_token: String, - pub auth_token: String, - pub channel_id: String, - pub target: Address, - pub json: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EncryptedWsMessage { - pub ws_auth_token: String, - pub auth_token: String, - pub channel_id: String, - pub target: Address, - pub encrypted: String, // Encrypted JSON as hex with the 32-byte authentication tag appended - pub nonce: String, // Hex of the 12-byte nonce -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum WebSocketClientMessage { - WsRegister(WsRegister), - WsMessage(WsMessage), - EncryptedWsMessage(EncryptedWsMessage), -} -// http_server End - -// encryptor Start -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct GetKeyAction { - pub channel_id: String, - pub public_key_hex: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DecryptAndForwardAction { - pub channel_id: String, - pub forward_to: Address, // node, process - pub json: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EncryptAndForwardAction { - pub channel_id: String, - pub forward_to: Address, // node, process - pub json: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DecryptAction { - pub channel_id: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EncryptAction { - pub channel_id: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum EncryptorMessage { - GetKey(GetKeyAction), - DecryptAndForward(DecryptAndForwardAction), - EncryptAndForward(EncryptAndForwardAction), - Decrypt(DecryptAction), - Encrypt(EncryptAction), -} -// encryptor End