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:
dr-frmr 2023-11-25 22:29:03 -05:00 committed by GitHub
commit f0ebfe2683
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 2420 additions and 4867 deletions

View File

@ -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"

View File

@ -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
View File

@ -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"

View File

@ -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]

View File

@ -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

View File

@ -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

View 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()
}

View File

@ -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",
]

View File

@ -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]

View File

@ -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>

View File

@ -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:?}");
}
}
}

View File

@ -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",
]

View File

@ -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"

View File

@ -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
}
]

View File

@ -1,5 +0,0 @@
{
"package": "http_proxy",
"publisher": "uqbar",
"version": [0, 1, 0]
}

File diff suppressed because one or more lines are too long

View File

@ -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()?;
}
}
}

View File

@ -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",
]

View File

@ -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]

View File

@ -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

View File

@ -5,7 +5,8 @@
"on_panic": "Restart",
"request_networking": true,
"request_messaging": [
"net:sys:uqbar"
"net:sys:uqbar",
"http_client:sys:uqbar"
],
"public": true
}

View File

@ -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
View 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
View File

@ -0,0 +1,4 @@
pub mod client;
pub mod server;
pub mod types;
pub mod utils;

883
src/http/server.rs Normal file
View 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
View 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
View 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()
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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);

View File

@ -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,
}
}

View File

@ -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(

View File

@ -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),

View File

@ -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 {

View File

@ -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