mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-12 16:17:39 +03:00
Merge pull request #73 from uqbar-dao/dr/http-refactor-2
Refactor http server and client to be clean, performant, good API
This commit is contained in:
commit
f0ebfe2683
@ -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"
|
||||
|
@ -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::<kt::PackageMetadata>(&metadata)?;
|
||||
|
117
modules/chess/Cargo.lock
generated
117
modules/chess/Cargo.lock
generated
@ -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"
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"package": "chess",
|
||||
"publisher": "uqbar",
|
||||
"version": [0, 1, 0]
|
||||
"version": [0, 2, 0]
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
82
modules/chess/src/utils.rs
Normal file
82
modules/chess/src/utils.rs
Normal file
@ -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()
|
||||
}
|
26
modules/homepage/Cargo.lock
generated
26
modules/homepage/Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
|
@ -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]
|
||||
|
@ -196,7 +196,7 @@
|
||||
|
||||
<h4>Apps:</h4>
|
||||
<!-- <a id="file-transfer" href="/file-transfer">File Transfer</a> -->
|
||||
<a id="chess" href="/chess:chess:uqbar/">Chess</a>
|
||||
<a id="chess" href="/chess:chess:uqbar/">Chess [NOT WORKING]</a>
|
||||
<a id="http-proxy" href="/http_proxy:http_proxy:uqbar/">HTTP Proxy</a>
|
||||
</div>
|
||||
<script>window.ourName = window.our = '${our}'</script>
|
||||
|
@ -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<Vec<u8>> {
|
||||
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::<Result<(), HttpServerError>>(&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::<serde_json::Value>(&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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
390
modules/http_proxy/Cargo.lock
generated
390
modules/http_proxy/Cargo.lock
generated
@ -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",
|
||||
]
|
@ -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"
|
@ -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
|
||||
}
|
||||
]
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"package": "http_proxy",
|
||||
"publisher": "uqbar",
|
||||
"version": [0, 1, 0]
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -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<Vec<u8>> {
|
||||
Ok(serde_json::to_vec(message)?)
|
||||
}
|
||||
|
||||
fn send_http_response(
|
||||
status: u16,
|
||||
headers: HashMap<String, String>,
|
||||
payload_bytes: Vec<u8>,
|
||||
) -> 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<String, String> = 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!("<h1>Node Offline</h1>").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()?;
|
||||
}
|
||||
}
|
||||
}
|
26
modules/qns_indexer/Cargo.lock
generated
26
modules/qns_indexer/Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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<String>,
|
||||
}
|
||||
|
||||
impl TryInto<Vec<u8>> for NetActions {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_into(self) -> Result<Vec<u8>, 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<u8> {
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
fn serialize_message(message: &NetActions) -> anyhow::Result<Vec<u8>> {
|
||||
Ok(rmp_serde::to_vec(message)?)
|
||||
}
|
||||
|
||||
fn serialize_json_message(message: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
|
||||
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::<Vec<_>>()),
|
||||
serialize_message,
|
||||
)?
|
||||
.target((&our.node, "net", "sys", "uqbar"))
|
||||
.try_ipc(NetActions::QnsBatchUpdate(
|
||||
state.nodes.values().cloned().collect::<Vec<_>>(),
|
||||
))?
|
||||
.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
|
||||
|
@ -5,7 +5,8 @@
|
||||
"on_panic": "Restart",
|
||||
"request_networking": true,
|
||||
"request_messaging": [
|
||||
"net:sys:uqbar"
|
||||
"net:sys:uqbar",
|
||||
"http_client:sys:uqbar"
|
||||
],
|
||||
"public": true
|
||||
}
|
||||
|
429
src/encryptor.rs
429
src/encryptor.rs
@ -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<u8>) -> Vec<u8> {
|
||||
let key = Key::<Aes256Gcm>::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<u8>) -> Vec<u8> {
|
||||
let nonce_bytes = data[data.len() - 12..].to_vec();
|
||||
let encrypted_bytes = data[..data.len() - 12].to_vec();
|
||||
let key = Key::<Aes256Gcm>::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<Ed25519KeyPair>,
|
||||
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<String, [u8; 32]> = 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::<EncryptorMessage>(&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::<sha2::Sha256>();
|
||||
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<u8>;
|
||||
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"))
|
||||
}
|
260
src/http/client.rs
Normal file
260
src/http/client.rs
Normal file
@ -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<String>,
|
||||
id: u64,
|
||||
target: Address,
|
||||
expects_response: Option<u64>,
|
||||
json: Vec<u8>,
|
||||
body: Option<Payload>,
|
||||
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::<Result<HttpResponse, HttpClientError>>(
|
||||
&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::<String>() + chars.as_str(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("-")
|
||||
}
|
||||
|
||||
fn serialize_headers(headers: &HeaderMap) -> HashMap<String, String> {
|
||||
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<String, String>) -> 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<String>,
|
||||
id: u64,
|
||||
target: Address,
|
||||
expects_response: Option<u64>,
|
||||
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::<Result<HttpResponse, HttpClientError>>(&Err(
|
||||
error,
|
||||
))
|
||||
.unwrap(),
|
||||
metadata: None,
|
||||
},
|
||||
None,
|
||||
)),
|
||||
payload: None,
|
||||
signed_capabilities: None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
4
src/http/mod.rs
Normal file
4
src/http/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
pub mod types;
|
||||
pub mod utils;
|
883
src/http/server.rs
Normal file
883
src/http/server.rs
Normal file
@ -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<DashMap<u64, (String, HttpSender)>>;
|
||||
type HttpSender = tokio::sync::oneshot::Sender<(HttpResponse, Vec<u8>)>;
|
||||
|
||||
/// 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<DashMap<u64, (ProcessId, WebSocketSender)>>;
|
||||
type WebSocketSender = tokio::sync::mpsc::Sender<warp::ws::Message>;
|
||||
|
||||
type PathBindings = Arc<RwLock<Router<BoundPath>>>;
|
||||
|
||||
struct BoundPath {
|
||||
pub app: ProcessId,
|
||||
pub authenticated: bool,
|
||||
pub local_only: bool,
|
||||
pub static_content: Option<Payload>, // 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<u8>,
|
||||
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<BoundPath> = 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<String>,
|
||||
our_port: u16,
|
||||
http_response_senders: HttpResponseSenders,
|
||||
path_bindings: PathBindings,
|
||||
ws_senders: WebSocketSenders,
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
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<String>,
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
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<SocketAddr>,
|
||||
path: warp::path::FullPath,
|
||||
headers: warp::http::HeaderMap,
|
||||
body: warp::hyper::body::Bytes,
|
||||
our: Arc<String>,
|
||||
http_response_senders: HttpResponseSenders,
|
||||
path_bindings: PathBindings,
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
send_to_loop: MessageSender,
|
||||
) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
// 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<String>,
|
||||
id: u64,
|
||||
body: warp::hyper::body::Bytes,
|
||||
) -> Result<KernelMessage, StatusCode> {
|
||||
let Ok(rpc_message) = serde_json::from_slice::<RpcMessage>(&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<Payload> = 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<String>,
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
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::<WsRegister>(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<Vec<u8>>,
|
||||
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::<HttpResponse>(&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::<HttpServerAction>(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;
|
||||
}
|
140
src/http/types.rs
Normal file
140
src/http/types.rs
Normal file
@ -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<String>, // will parse to SocketAddr
|
||||
pub method: String, // will parse to http::Method
|
||||
pub raw_path: String,
|
||||
pub headers: HashMap<String, String>,
|
||||
// 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<String>, // must parse to http::Version
|
||||
pub url: String, // must parse to url::Url
|
||||
pub headers: HashMap<String, String>,
|
||||
// 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<String, String>,
|
||||
// BODY is stored in the payload, as bytes
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RpcResponseBody {
|
||||
pub ipc: Vec<u8>,
|
||||
pub payload: Option<Payload>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
116
src/http/utils.rs
Normal file
116
src/http/utils.rs
Normal file
@ -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<String>,
|
||||
pub process: String,
|
||||
pub inherit: Option<bool>,
|
||||
pub expects_response: Option<u64>,
|
||||
pub ipc: Option<String>,
|
||||
pub metadata: Option<String>,
|
||||
pub context: Option<String>,
|
||||
pub mime: Option<String>,
|
||||
pub data: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String, jwt::Error> {
|
||||
let Ok(secret) = Hmac::<Sha256>::new_from_slice(jwt_secret) else {
|
||||
return Err(jwt::Error::Format);
|
||||
};
|
||||
|
||||
let claims: Result<JwtClaims, jwt::Error> = 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::<Sha256>::new_from_slice(jwt_secret) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let claims: Result<JwtClaims, _> = 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<String, String> {
|
||||
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<String, String>) -> 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<u16> {
|
||||
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<u8> {
|
||||
s.chars().map(|c| c as u8).collect()
|
||||
}
|
@ -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<Address>,
|
||||
expects_response: Option<u64>,
|
||||
source: Address,
|
||||
json: Vec<u8>,
|
||||
body: Option<Vec<u8>>,
|
||||
_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::<Result<HttpClientResponse, HttpClientError>>(&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::<String>() + chars.as_str(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("-")
|
||||
}
|
||||
|
||||
fn serialize_headers(headers: &HeaderMap) -> HashMap<String, String> {
|
||||
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<String, String>) -> 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::<Result<HttpClientResponse, HttpClientError>>(&Err(error))
|
||||
.unwrap(),
|
||||
metadata: None,
|
||||
},
|
||||
None,
|
||||
)),
|
||||
payload: None,
|
||||
signed_capabilities: None,
|
||||
}
|
||||
}
|
@ -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<HttpResponse>;
|
||||
type HttpResponseSenders = Arc<Mutex<HashMap<u64, (String, HttpSender)>>>;
|
||||
type PathBindings = Arc<RwLock<Router<BoundPath>>>;
|
||||
|
||||
// node -> ID -> random ID
|
||||
|
||||
/// http driver
|
||||
pub async fn http_server(
|
||||
our_name: String,
|
||||
our_port: u16,
|
||||
jwt_secret_bytes: Vec<u8>,
|
||||
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<BoundPath> = 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<u8>,
|
||||
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::<WebSocketClientMessage>(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<Payload>,
|
||||
http_response_senders: HttpResponseSenders,
|
||||
path_bindings: PathBindings,
|
||||
websockets: WebSockets,
|
||||
ws_proxies: WebSocketProxies,
|
||||
jwt_secret_bytes: Vec<u8>,
|
||||
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::<HttpResponse>(&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<u8>,
|
||||
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<u8>,
|
||||
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<SocketAddr>,
|
||||
path: warp::path::FullPath,
|
||||
headers: warp::http::HeaderMap,
|
||||
query_params: HashMap<String, String>,
|
||||
body: warp::hyper::body::Bytes,
|
||||
our: String,
|
||||
http_response_senders: HttpResponseSenders,
|
||||
path_bindings: PathBindings,
|
||||
jwt_secret_bytes: Vec<u8>,
|
||||
send_to_loop: MessageSender,
|
||||
_print_tx: PrintSender,
|
||||
) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
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<u8> = 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<u16> {
|
||||
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
|
||||
}
|
@ -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<Mutex<SplitSink<WebSocket, warp::ws::Message>>>;
|
||||
pub type WebSockets = Arc<Mutex<HashMap<String, HashMap<String, HashMap<u64, SharedWriteStream>>>>>;
|
||||
pub type WebSocketProxies = Arc<Mutex<HashMap<String, HashSet<String>>>>;
|
||||
|
||||
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<String>,
|
||||
pub process: String,
|
||||
pub inherit: Option<bool>,
|
||||
pub expects_response: Option<u64>,
|
||||
pub ipc: Option<String>,
|
||||
pub metadata: Option<String>,
|
||||
pub context: Option<String>,
|
||||
pub mime: Option<String>,
|
||||
pub data: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_auth_token(auth_token: String, jwt_secret: Vec<u8>) -> Result<String, Error> {
|
||||
let secret: Hmac<Sha256> = match Hmac::new_from_slice(jwt_secret.as_slice()) {
|
||||
Ok(secret) => secret,
|
||||
Err(_) => {
|
||||
return Ok("Error recovering jwt secret".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let claims: Result<JwtClaims, Error> = 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<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 secret = match Hmac::<Sha256>::new_from_slice(&jwt_secret) {
|
||||
Ok(secret) => secret,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let claims: Result<JwtClaims, _> = 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<u8>,
|
||||
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<String, String> {
|
||||
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<String, String>) -> 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<u8> {
|
||||
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<serde_json::Value>,
|
||||
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,
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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<u8>;
|
||||
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<Self, ProcessIdParseError> {
|
||||
// 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<String>, // MIME type
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub inherit: bool,
|
||||
pub expects_response: Option<u64>, // number of seconds until timeout
|
||||
pub ipc: Vec<u8>,
|
||||
pub metadata: Option<String>, // JSON-string
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Response {
|
||||
pub inherit: bool,
|
||||
pub ipc: Vec<u8>,
|
||||
pub metadata: Option<String>, // JSON-string
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Message {
|
||||
Request(Request),
|
||||
Response((Response, Option<Context>)),
|
||||
}
|
||||
|
||||
#[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<u8>, // 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<Payload>,
|
||||
}
|
||||
|
||||
#[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<Payload>)>),
|
||||
}
|
||||
|
||||
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<SignedCapability>,
|
||||
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<Capability>,
|
||||
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<String>),
|
||||
GetHash(Option<u128>),
|
||||
GetEntry {
|
||||
// file bytes in payload, if entry was a file
|
||||
is_file: bool,
|
||||
children: Vec<String>,
|
||||
},
|
||||
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<String>,
|
||||
pub website: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
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<wit::Payload>) -> Option<Payload> {
|
||||
match wit {
|
||||
None => None,
|
||||
Some(wit) => Some(Payload {
|
||||
mime: wit.mime,
|
||||
bytes: wit.bytes,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn en_wit_payload(load: Option<Payload>) -> Option<wit::Payload> {
|
||||
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,
|
||||
}
|
||||
}
|
26
src/main.rs
26
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::<bool>();
|
||||
|
||||
@ -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(
|
||||
|
@ -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<String>
|
||||
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),
|
||||
|
@ -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 {
|
||||
|
506
src/types.rs
506
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<u8>;
|
||||
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<Self, ProcessIdParseError> {
|
||||
// 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<ProcessId> 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<T>(node: &str, process: T) -> Address
|
||||
where
|
||||
T: Into<ProcessId>,
|
||||
{
|
||||
Address {
|
||||
node: node.to_string(),
|
||||
process: process.into(),
|
||||
}
|
||||
}
|
||||
pub fn _from_str(input: &str) -> Result<Self, AddressParseError> {
|
||||
// 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<T> From<(&str, T)> for Address
|
||||
where
|
||||
T: Into<ProcessId>,
|
||||
{
|
||||
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<String>, // 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<Vec<SignedCapability>>,
|
||||
}
|
||||
|
||||
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<Address>;
|
||||
|
||||
//
|
||||
// boot/startup specific types???
|
||||
//
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BootOutboundRequest {
|
||||
pub target_process: ProcessId,
|
||||
pub json: Option<String>,
|
||||
pub bytes: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[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<bool>,
|
||||
},
|
||||
Drop {
|
||||
_Drop {
|
||||
// not used yet!
|
||||
on: ProcessId,
|
||||
cap: Capability,
|
||||
@ -598,14 +787,6 @@ pub struct ProcessContext {
|
||||
pub context: Option<Context>,
|
||||
}
|
||||
|
||||
//
|
||||
// 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<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct HttpClientResponse {
|
||||
pub status: u16,
|
||||
pub headers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[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<String, String>,
|
||||
pub body: Option<Vec<u8>>, // 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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct WebSocketPush {
|
||||
pub target: WebSocketServerTarget,
|
||||
pub is_text: Option<bool>,
|
||||
}
|
||||
|
||||
#[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<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[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<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EncryptAndForwardAction {
|
||||
pub channel_id: String,
|
||||
pub forward_to: Address, // node, process
|
||||
pub json: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[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
|
||||
|
Loading…
Reference in New Issue
Block a user