update frontend-serving apps to new process_lib http::server utils

This commit is contained in:
dr-frmr 2024-08-05 19:32:56 +03:00
parent 6c15904d76
commit 779018b84d
No known key found for this signature in database
10 changed files with 668 additions and 772 deletions

94
Cargo.lock generated
View File

@ -61,7 +61,7 @@ dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy 0.7.35",
"zerocopy",
]
[[package]]
@ -117,9 +117,9 @@ dependencies = [
[[package]]
name = "alloy-chains"
version = "0.1.24"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47ff94ce0f141c2671c23d02c7b88990dd432856639595c5d010663d017c2c58"
checksum = "3312b2a48f29abe7c3ea7c7fbc1f8cc6ea09b85d74b6232e940df35f2f3826fd"
dependencies = [
"num_enum",
"strum",
@ -1162,9 +1162,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.7.0"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
dependencies = [
"serde",
]
@ -2078,9 +2078,9 @@ dependencies = [
[[package]]
name = "dunce"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "ecdsa"
@ -2255,9 +2255,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.0.30"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920"
dependencies = [
"crc32fast",
"libz-ng-sys",
@ -3313,7 +3313,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.9.0"
source = "git+https://github.com/kinode-dao/process_lib?branch=develop#51800f9c144b3b69ed52406b4b2ae4c5aa078aec"
source = "git+https://github.com/kinode-dao/process_lib?branch=develop#ddc4c11f5e2cfd6461d6d8f44d154146f3dbc087"
dependencies = [
"alloy",
"alloy-primitives",
@ -4197,11 +4197,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.18"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy 0.6.6",
"zerocopy",
]
[[package]]
@ -4562,9 +4562,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.5"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
@ -4671,7 +4671,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile 2.1.2",
"rustls-pemfile 2.1.3",
"serde",
"serde_json",
"serde_urlencoded",
@ -4880,9 +4880,9 @@ dependencies = [
[[package]]
name = "rustls-pemfile"
version = "2.1.2"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425"
dependencies = [
"base64 0.22.1",
"rustls-pki-types",
@ -5048,9 +5048,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.121"
version = "1.0.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609"
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
dependencies = [
"itoa",
"memchr",
@ -5470,12 +5470,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tempfile"
version = "3.10.1"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53"
dependencies = [
"cfg-if",
"fastrand",
"once_cell",
"rustix",
"windows-sys 0.52.0",
]
@ -6822,11 +6823,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -6878,6 +6879,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
@ -7199,34 +7209,14 @@ dependencies = [
"tap",
]
[[package]]
name = "zerocopy"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
dependencies = [
"byteorder",
"zerocopy-derive 0.6.6",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy-derive"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.72",
"byteorder",
"zerocopy-derive",
]
[[package]]
@ -7336,7 +7326,7 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe 7.2.0",
"zstd-safe 7.2.1",
]
[[package]]
@ -7351,18 +7341,18 @@ dependencies = [
[[package]]
name = "zstd-safe"
version = "7.2.0"
version = "7.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.12+zstd.1.5.6"
version = "2.0.13+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [
"cc",
"pkg-config",

View File

@ -1,11 +1,9 @@
use crate::state::{MirrorCheckFile, PackageListing, State};
use crate::DownloadResponse;
use kinode_process_lib::{
http::{
bind_http_path, bind_ws_path, send_response, serve_ui, IncomingHttpRequest, Method,
StatusCode,
},
println, Address, NodeId, PackageId, ProcessId, Request,
http::server,
http::{self, Method, StatusCode},
Address, LazyLoadBlob, NodeId, PackageId, Request,
};
use kinode_process_lib::{SendError, SendErrorKind};
use serde_json::json;
@ -15,7 +13,9 @@ const ICON: &str = include_str!("icon");
/// Bind static and dynamic HTTP paths for the app store,
/// bind to our WS updates path, and add icon and widget to homepage.
pub fn init_frontend(our: &Address) {
pub fn init_frontend(our: &Address, http_server: &mut server::HttpServer) {
let config = server::HttpBindingConfig::default();
for path in [
"/apps",
"/apps/:id",
@ -27,30 +27,30 @@ pub fn init_frontend(our: &Address) {
"/apps/rebuild-index",
"/mirrorcheck/:node",
] {
bind_http_path(path, true, false).expect("failed to bind http path");
http_server
.bind_http_path(path, config.clone())
.expect("failed to bind http path");
}
serve_ui(&our, "ui", true, false, vec!["/", "/app/:id", "/publish"])
http_server
.serve_ui(
&our,
"ui",
vec!["/", "/app/:id", "/publish"],
config.clone(),
)
.expect("failed to serve static UI");
bind_ws_path("/", true, true).expect("failed to bind ws path");
http_server
.bind_ws_path("/", server::WsBindingConfig::default())
.expect("failed to bind ws path");
// add ourselves to the homepage
Request::to(("our", "homepage", "homepage", "sys"))
.body(
serde_json::json!({
"Add": {
"label": "App Store",
"icon": ICON,
"path": "/",
"widget": make_widget()
}
})
.to_string()
.as_bytes()
.to_vec(),
)
.send()
.unwrap();
kinode_process_lib::homepage::add_to_homepage(
"App Store",
Some(ICON),
Some("/"),
Some(&make_widget()),
);
}
fn make_widget() -> String {
@ -183,20 +183,23 @@ fn make_widget() -> String {
/// - stop auto-updating a downloaded app: DELETE /apps/:id/auto-update
///
/// - RebuildIndex: POST /apps/rebuild-index
pub fn handle_http_request(state: &mut State, req: &IncomingHttpRequest) -> anyhow::Result<()> {
pub fn handle_http_request(
state: &mut State,
req: &server::IncomingHttpRequest,
) -> (server::HttpResponse, Option<LazyLoadBlob>) {
match serve_paths(state, req) {
Ok((status_code, _headers, body)) => send_response(
status_code,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
body,
Ok((status_code, _headers, body)) => (
server::HttpResponse::new(status_code).header("Content-Type", "application/json"),
Some(LazyLoadBlob {
mime: None,
bytes: body,
}),
),
Err(_e) => (
server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR),
None,
),
Err(_e) => send_response(StatusCode::INTERNAL_SERVER_ERROR, None, vec![]),
}
Ok(())
}
fn get_package_id(url_params: &HashMap<String, String>) -> anyhow::Result<PackageId> {
@ -236,8 +239,8 @@ fn gen_package_info(id: &PackageId, listing: &PackageListing) -> serde_json::Val
fn serve_paths(
state: &mut State,
req: &IncomingHttpRequest,
) -> anyhow::Result<(StatusCode, Option<HashMap<String, String>>, Vec<u8>)> {
req: &server::IncomingHttpRequest,
) -> anyhow::Result<(http::StatusCode, Option<HashMap<String, String>>, Vec<u8>)> {
let method = req.method()?;
let bound_path: &str = req.bound_path(Some(&state.our.process.to_string()));

View File

@ -22,9 +22,8 @@ use ft_worker_lib::{
spawn_receive_transfer, spawn_transfer, FTWorkerCommand, FTWorkerResult, FileTransferContext,
};
use kinode_process_lib::{
await_message, call_init, eth, get_blob,
http::{self, WsMessageType},
kimap, println, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response,
await_message, call_init, eth, get_blob, http, kimap, println, vfs, Address, LazyLoadBlob,
Message, PackageId, Request, Response,
};
use serde::{Deserialize, Serialize};
use state::{AppStoreLogError, PackageState, RequestedPackage, State};
@ -71,7 +70,7 @@ pub enum Req {
FTWorkerCommand(FTWorkerCommand),
FTWorkerResult(FTWorkerResult),
Eth(eth::EthSubResult),
Http(http::HttpServerRequest),
Http(http::server::HttpServerRequest),
}
#[derive(Debug, Serialize, Deserialize)]
@ -80,14 +79,15 @@ pub enum Resp {
LocalResponse(LocalResponse),
RemoteResponse(RemoteResponse),
FTWorkerResult(FTWorkerResult),
HttpClient(Result<http::HttpClientResponse, http::HttpClientError>),
HttpClient(Result<http::client::HttpClientResponse, http::client::HttpClientError>),
}
call_init!(init);
fn init(our: Address) {
println!("started");
http_api::init_frontend(&our);
let mut http_server = http::server::HttpServer::new(5);
http_api::init_frontend(&our, &mut http_server);
println!("indexing on contract address {}", KIMAP_ADDRESS);
@ -105,7 +105,7 @@ fn init(our: Address) {
println!("got network error: {send_error}");
}
Ok(message) => {
if let Err(e) = handle_message(&mut state, &message) {
if let Err(e) = handle_message(&mut state, &mut http_server, &message) {
println!("error handling message: {:?}", e);
}
}
@ -117,7 +117,11 @@ fn init(our: Address) {
/// function defined for each kind of message. check whether the source
/// of the message is allowed to send that kind of message to us.
/// finally, fire a response if expected from a request.
fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
fn handle_message(
state: &mut State,
http_server: &mut http::server::HttpServer,
message: &Message,
) -> anyhow::Result<()> {
if message.is_request() {
match serde_json::from_slice::<Req>(message.body())? {
Req::LocalRequest(local_request) => {
@ -148,23 +152,24 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
total_chunks,
}) => {
// forward progress to UI
let ws_blob = LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::json!({
"kind": "progress",
"data": {
"file_name": file_name,
"chunks_received": chunks_received,
"total_chunks": total_chunks,
}
})
.to_string()
.as_bytes()
.to_vec(),
};
for channel_id in state.ui_ws_channels.iter() {
http::send_ws_push(*channel_id, WsMessageType::Text, ws_blob.clone());
}
http_server.ws_push_all_channels(
"/",
http::server::WsMessageType::Text,
LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::json!({
"kind": "progress",
"data": {
"file_name": file_name,
"chunks_received": chunks_received,
"total_chunks": total_chunks,
}
})
.to_string()
.as_bytes()
.to_vec(),
},
);
}
Req::FTWorkerResult(r) => {
println!("got weird ft_worker result: {r:?}");
@ -186,19 +191,19 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
.subscribe_loop(1, utils::app_store_filter(state));
}
}
Req::Http(incoming) => {
Req::Http(server_request) => {
if !message.is_local(&state.our)
|| message.source().process != "http_server:distro:sys"
{
return Err(anyhow::anyhow!("http_server from non-local node"));
}
if let http::HttpServerRequest::Http(req) = incoming {
http_api::handle_http_request(state, &req)?;
} else if let http::HttpServerRequest::WebSocketOpen { channel_id, .. } = incoming {
state.ui_ws_channels.insert(channel_id);
} else if let http::HttpServerRequest::WebSocketClose { 0: channel_id } = incoming {
state.ui_ws_channels.remove(&channel_id);
}
http_server.handle_request(
server_request,
|incoming| http_api::handle_http_request(state, &incoming),
|_channel_id, _message_type, _blob| {
// not expecting any websocket messages from FE currently
},
);
}
}
} else {
@ -208,7 +213,10 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
Some(context) => std::str::from_utf8(context).unwrap_or_default(),
None => return Err(anyhow::anyhow!("http_client response without context")),
};
if let Ok(http::HttpClientResponse::Http(http::HttpResponse { status, .. })) = resp
if let Ok(http::client::HttpClientResponse::Http(http::client::HttpResponse {
status,
..
})) = resp
{
if status == 200 {
handle_receive_download(state, &name)?;
@ -435,23 +443,12 @@ pub fn start_download(
};
// if `from` is a node, send a request to it
// but if it is a url, use http_client to GET it
let Ok(url) = url::Url::parse(&from) else {
return DownloadResponse::Denied(Reason::NotMirroring);
};
if from.starts_with("http") {
// use http_client to GET it
Request::to(("our", "http_client", "distro", "sys"))
.body(
serde_json::to_vec(&http::HttpClientAction::Http(http::OutgoingHttpRequest {
method: "GET".to_string(),
version: None,
url: from.clone(),
headers: std::collections::HashMap::new(),
}))
.unwrap(),
)
.context(package_id.to_string().as_bytes())
.expects_response(60)
.send()
.unwrap();
http::client::send_request(http::Method::GET, url, None, Some(60), vec![]);
return DownloadResponse::Started;
} else {
if let Ok(Ok(Message::Response { body, .. })) =

View File

@ -116,8 +116,6 @@ pub struct State {
pub requested_packages: HashMap<PackageId, RequestedPackage>,
/// the APIs we have outstanding requests to download (not persisted)
pub requested_apis: HashMap<PackageId, RequestedPackage>,
/// UI websocket connected channel_IDs
pub ui_ws_channels: HashSet<u32>,
}
#[derive(Deserialize)]
@ -153,7 +151,6 @@ impl State {
downloaded_apis: s.downloaded_apis,
requested_packages: HashMap::new(),
requested_apis: HashMap::new(),
ui_ws_channels: HashSet::new(),
}
}
@ -168,7 +165,6 @@ impl State {
downloaded_apis: HashSet::new(),
requested_packages: HashMap::new(),
requested_apis: HashMap::new(),
ui_ws_channels: HashSet::new(),
};
state.populate_packages_from_filesystem()?;
Ok(state)
@ -380,7 +376,7 @@ impl State {
let block_number: u64 = log.block_number.ok_or(AppStoreLogError::NoBlockNumber)?;
let note: kimap::Note =
kimap::decode_note_log(&log).map_err(AppStoreLogError::DecodeLogError)?;
kimap::decode_note_log(&log).map_err(|e| AppStoreLogError::DecodeLogError(e))?;
let package_id = note
.parent_path

View File

@ -131,7 +131,7 @@ pub fn fetch_metadata_from_url(
) -> Result<kt::Erc721Metadata, AppStoreLogError> {
if let Ok(url) = url::Url::parse(metadata_url) {
if let Ok(_) =
http::send_request_await_response(http::Method::GET, url, None, timeout, vec![])
http::client::send_request_await_response(http::Method::GET, url, None, timeout, vec![])
{
if let Some(body) = get_blob() {
let hash = keccak_256_hash(&body.bytes);

View File

@ -1,16 +1,14 @@
#![feature(let_chains)]
use crate::kinode::process::chess::{
MoveRequest, NewGameRequest, Request as ChessRequest, Response as ChessResponse,
};
use kinode_process_lib::{
await_message, call_init, get_blob, get_typed_state, http, println, set_state, Address,
LazyLoadBlob, Message, NodeId, Request, Response,
await_message, call_init, get_blob, get_typed_state, http, http::server, println, set_state,
Address, LazyLoadBlob, Message, NodeId, Request, Response,
};
use pleco::Board;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
extern crate base64;
use crate::kinode::process::chess::{
MoveRequest, NewGameRequest, Request as ChessRequest, Response as ChessResponse,
};
const ICON: &str = include_str!("icon");
@ -30,6 +28,7 @@ struct Game {
#[derive(Debug, Serialize, Deserialize)]
struct ChessState {
pub our: Address,
pub games: HashMap<String, Game>, // game is by opposing player id
pub clients: HashSet<u32>, // doesn't get persisted
}
@ -43,14 +42,16 @@ fn save_chess_state(state: &ChessState) {
set_state(&bincode::serialize(&state.games).unwrap());
}
fn load_chess_state() -> ChessState {
fn load_chess_state(our: Address) -> ChessState {
match get_typed_state(|bytes| bincode::deserialize::<HashMap<String, Game>>(bytes)) {
Some(games) => ChessState {
our,
games,
clients: HashSet::new(),
},
None => {
let state = ChessState {
our,
games: HashMap::new(),
clients: HashSet::new(),
};
@ -60,28 +61,20 @@ fn load_chess_state() -> ChessState {
}
}
fn send_ws_update(our: &Address, game: &Game, open_channels: &HashSet<u32>) -> anyhow::Result<()> {
for channel in open_channels {
Request::new()
.target((&our.node, "http_server", "distro", "sys"))
.body(serde_json::to_vec(
&http::HttpServerAction::WebSocketPush {
channel_id: *channel,
message_type: http::WsMessageType::Binary,
},
)?)
.blob(LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::json!({
"kind": "game_update",
"data": game,
})
.to_string()
.into_bytes(),
fn send_ws_update(http_server: &mut server::HttpServer, game: &Game) {
http_server.ws_push_all_channels(
"/",
server::WsMessageType::Binary,
LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::json!({
"kind": "game_update",
"data": game,
})
.send()?;
}
Ok(())
.to_string()
.into_bytes(),
},
)
}
// Boilerplate: generate the wasm bindings for a process
@ -91,6 +84,7 @@ wit_bindgen::generate!({
generate_unused_types: true,
additional_derives: [PartialEq, serde::Deserialize, serde::Serialize],
});
// After generating bindings, use this macro to define the Component struct
// and its init() function, which the kernel will look for on startup.
call_init!(initialize);
@ -99,38 +93,34 @@ fn initialize(our: Address) {
println!("started");
// add ourselves to the homepage
Request::to(("our", "homepage", "homepage", "sys"))
.body(
serde_json::json!({
"Add": {
"label": "Chess",
"icon": ICON,
"path": "/", // just our root
}
})
.to_string()
.as_bytes()
.to_vec(),
)
.send()
.unwrap();
kinode_process_lib::homepage::add_to_homepage("Chess", Some(ICON), Some("/"), None);
// create an HTTP server struct with which to manipulate `http_server:distro:sys`
let mut http_server = server::HttpServer::new(5);
let http_config = server::HttpBindingConfig::default();
// Serve the index.html and other UI files found in pkg/ui at the root path.
// authenticated=true, local_only=false
http::serve_ui(&our, "ui", true, false, vec!["/"]).unwrap();
http_server
.serve_ui(&our, "ui", http_config.clone())
.expect("failed to serve ui");
// Allow HTTP requests to be made to /games; they will be handled dynamically.
http::bind_http_path("/games", true, false).unwrap();
http_server
.bind_http_path("/games", http_config.clone())
.expect("failed to bind /games");
// Allow websockets to be opened at / (our process ID will be prepended).
http::bind_ws_path("/", true, false).unwrap();
http_server
.bind_ws_path("/", server::WsBindingConfig::default())
.expect("failed to bind ws");
// Grab our state, then enter the main event loop.
let mut state: ChessState = load_chess_state();
main_loop(&our, &mut state);
let mut state: ChessState = load_chess_state(our);
main_loop(&mut state, &mut http_server);
}
fn main_loop(our: &Address, state: &mut ChessState) {
fn main_loop(state: &mut ChessState, http_server: &mut server::HttpServer) {
loop {
// Call await_message() to wait for any incoming messages.
// If we get a network error, make a print and throw it away.
@ -141,96 +131,71 @@ fn main_loop(our: &Address, state: &mut ChessState) {
println!("got network error: {send_error:?}");
continue;
}
Ok(message) => match handle_request(&our, &message, state) {
Ok(message) => match handle_request(&message, state, http_server) {
Ok(()) => continue,
Err(e) => println!("error handling request: {:?}", e),
Err(e) => println!("error handling request: {e}"),
},
}
}
}
/// Handle chess protocol messages from ourself *or* other nodes.
fn handle_request(our: &Address, message: &Message, state: &mut ChessState) -> anyhow::Result<()> {
fn handle_request(
message: &Message,
state: &mut ChessState,
http_server: &mut server::HttpServer,
) -> anyhow::Result<()> {
// Throw away responses. We never expect any responses *here*, because for every
// chess protocol request, we *await* its response in-place. This is appropriate
// for direct node<>node comms, less appropriate for other circumstances...
// for direct node-to-node comms, less appropriate for other circumstances...
if !message.is_request() {
return Ok(());
}
// If the request is from another node, handle it as an incoming request.
// Note that we can enforce the ProcessId as well, but it shouldn't be a trusted
// piece of information, since another node can easily spoof any ProcessId on a request.
// It can still be useful simply as a protocol-level switch to handle different kinds of
// requests from the same node, with the knowledge that the remote node can finagle with
// which ProcessId a given message can be from. It's their code, after all.
if message.source().node != our.node {
if message.source().node != state.our.node {
// Deserialize the request IPC to our format, and throw it away if it
// doesn't fit.
let Ok(chess_request) = serde_json::from_slice::<ChessRequest>(message.body()) else {
return Err(anyhow::anyhow!("invalid chess request"));
};
handle_chess_request(our, &message.source().node, state, &chess_request)
handle_chess_request(&message.source().node, state, http_server, &chess_request)
}
// ...and if the request is from ourselves, handle it as our own!
// Note that since this is a local request, we *can* trust the ProcessId.
// Here, we'll accept messages from the local terminal so as to make this a "CLI" app.
} else if message.source().node == our.node
&& message.source().process == "terminal:terminal:sys"
{
else {
// Here, we accept messages *from any local process that can message this one*.
// Since the manifest specifies that this process is *public*, any local process
// can "play chess" for us.
//
// If you wanted to restrict this privilege, you could check for a specific process,
// package, and/or publisher here, *or* change the manifest to only grant messaging
// capabilities to specific processes.
// if the message is from the HTTP server runtime module, we should handle it
// as an HTTP request and not a chess request
if message.source().process == "http_server:distro:sys" {
return handle_http_request(state, http_server, message);
}
let Ok(chess_request) = serde_json::from_slice::<ChessRequest>(message.body()) else {
return Err(anyhow::anyhow!("invalid chess request"));
};
handle_local_request(our, state, &chess_request)
} else if message.source().node == our.node
&& message.source().process == "http_server:distro:sys"
{
// receive HTTP requests and websocket connection messages from our server
match serde_json::from_slice::<http::HttpServerRequest>(message.body())? {
http::HttpServerRequest::Http(ref incoming) => {
match handle_http_request(our, state, incoming) {
Ok(()) => Ok(()),
Err(e) => {
http::send_response(
http::StatusCode::SERVICE_UNAVAILABLE,
None,
"Service Unavailable".to_string().as_bytes().to_vec(),
);
Err(anyhow::anyhow!("error handling http request: {e:?}"))
}
}
}
http::HttpServerRequest::WebSocketOpen { channel_id, .. } => {
// We know this is authenticated and unencrypted because we only
// bound one path, the root path. So we know that client
// frontend opened a websocket and can send updates
state.clients.insert(channel_id);
Ok(())
}
http::HttpServerRequest::WebSocketClose(channel_id) => {
// client frontend closed a websocket
state.clients.remove(&channel_id);
Ok(())
}
http::HttpServerRequest::WebSocketPush { .. } => {
// client frontend sent a websocket message
// we don't expect this! we only use websockets to push updates
Ok(())
}
}
} else {
// If we get a request from ourselves that isn't from the terminal, we'll just
// throw it away. This is a good place to put a printout to show that we've
// received a request from ourselves that we don't know how to handle.
return Err(anyhow::anyhow!(
"got request from not-the-terminal, ignoring"
));
let _game = handle_local_request(state, &chess_request)?;
Ok(())
}
}
/// Handle chess protocol messages from other nodes.
fn handle_chess_request(
our: &Address,
source_node: &NodeId,
state: &mut ChessState,
http_server: &mut server::HttpServer,
action: &ChessRequest,
) -> anyhow::Result<()> {
println!("handling action from {source_node}: {action:?}");
@ -258,7 +223,7 @@ fn handle_chess_request(
// The simplest and most trivial way to keep state. You'll want to
// use a database or something in a real app, and consider performance
// when doing intensive data-based operations.
send_ws_update(&our, &game, &state.clients)?;
send_ws_update(http_server, &game);
state.games.insert(game_id.to_string(), game);
save_chess_state(&state);
// Send a response to tell them we've accepted the game.
@ -293,7 +258,7 @@ fn handle_chess_request(
}
// Persist state.
game.board = board.fen();
send_ws_update(&our, &game, &state.clients)?;
send_ws_update(http_server, &game);
save_chess_state(&state);
// Send a response to tell them we've accepted the move.
Response::new()
@ -307,7 +272,7 @@ fn handle_chess_request(
match state.games.get_mut(game_id) {
Some(game) => {
game.ended = true;
send_ws_update(&our, &game, &state.clients)?;
send_ws_update(http_server, &game);
save_chess_state(&state);
}
None => {}
@ -318,18 +283,18 @@ fn handle_chess_request(
}
/// Handle actions we are performing. Here's where we'll send_and_await various requests.
fn handle_local_request(
our: &Address,
state: &mut ChessState,
action: &ChessRequest,
) -> anyhow::Result<()> {
fn handle_local_request(state: &mut ChessState, action: &ChessRequest) -> anyhow::Result<Game> {
match action {
ChessRequest::NewGame(NewGameRequest { white, black }) => {
// Create a new game. We'll enforce that one of the two players is us.
if white != &our.node && black != &our.node {
if white != &state.our.node && black != &state.our.node {
return Err(anyhow::anyhow!("cannot start a game without us!"));
}
let game_id = if white == &our.node { black } else { white };
let game_id = if white == &state.our.node {
black
} else {
white
};
// If we already have a game with this player, throw an error.
if let Some(game) = state.games.get(game_id)
&& !game.ended
@ -338,11 +303,11 @@ fn handle_local_request(
};
// Send the other player a NewGame request
// The request is exactly the same as what we got from terminal.
// We'll give them 5 seconds to respond...
// We'll give their node 30 seconds to respond...
let Ok(Message::Response { ref body, .. }) = Request::new()
.target((game_id, our.process.clone()))
.target((game_id, state.our.process.clone()))
.body(serde_json::to_vec(&action)?)
.send_and_await_response(5)?
.send_and_await_response(30)?
else {
return Err(anyhow::anyhow!(
"other player did not respond properly to new game request"
@ -361,9 +326,9 @@ fn handle_local_request(
black: black.to_string(),
ended: false,
};
state.games.insert(game_id.to_string(), game);
state.games.insert(game_id.to_string(), game.clone());
save_chess_state(&state);
Ok(())
Ok(game)
}
ChessRequest::Move(MoveRequest { game_id, move_str }) => {
// Make a move. We'll enforce that it's our turn. The game_id is the
@ -371,8 +336,8 @@ fn handle_local_request(
let Some(game) = state.games.get_mut(game_id) else {
return Err(anyhow::anyhow!("no game with {game_id}"));
};
if (game.turns % 2 == 0 && game.white != our.node)
|| (game.turns % 2 == 1 && game.black != our.node)
if (game.turns % 2 == 0 && game.white != state.our.node)
|| (game.turns % 2 == 1 && game.black != state.our.node)
{
return Err(anyhow::anyhow!("not our turn!"));
} else if game.ended {
@ -384,11 +349,11 @@ fn handle_local_request(
}
// Send the move to the other player, then check if the game is over.
// The request is exactly the same as what we got from terminal.
// We'll give them 5 seconds to respond...
// We'll give their node 30 seconds to respond...
let Ok(Message::Response { ref body, .. }) = Request::new()
.target((game_id, our.process.clone()))
.target((game_id, state.our.process.clone()))
.body(serde_json::to_vec(&action)?)
.send_and_await_response(5)?
.send_and_await_response(30)?
else {
return Err(anyhow::anyhow!(
"other player did not respond properly to our move"
@ -402,8 +367,9 @@ fn handle_local_request(
game.ended = true;
}
game.board = board.fen();
let game = game.clone();
save_chess_state(&state);
Ok(())
Ok(game)
}
ChessRequest::Resign(ref with_who) => {
// Resign from a game with a given player.
@ -412,250 +378,196 @@ fn handle_local_request(
};
// send the other player an end game request -- no response expected
Request::new()
.target((with_who, our.process.clone()))
.target((with_who, state.our.process.clone()))
.body(serde_json::to_vec(&action)?)
.send()?;
game.ended = true;
let game = game.clone();
save_chess_state(&state);
Ok(())
Ok(game)
}
}
}
/// Handle HTTP requests from our own frontend.
fn handle_http_request(
our: &Address,
state: &mut ChessState,
http_request: &http::IncomingHttpRequest,
http_server: &mut server::HttpServer,
message: &Message,
) -> anyhow::Result<()> {
if http_request.bound_path(Some(&our.process.to_string())) != "/games" {
http::send_response(
http::StatusCode::NOT_FOUND,
let request = http_server.parse_request(message.body())?;
// the HTTP server helper struct allows us to pass functions that
// handle the various types of requests we get from the frontend
http_server.handle_request(
request,
|incoming| {
// client frontend sent an HTTP request, process it and
// return an HTTP response
// these functions can reuse the logic from handle_local_request
// after converting the request into the appropriate format!
match incoming.method().unwrap_or_default() {
http::Method::GET => handle_get(state),
http::Method::POST => handle_post(state),
http::Method::PUT => handle_put(state),
http::Method::DELETE => handle_delete(state, &incoming),
_ => (
server::HttpResponse::new(http::StatusCode::METHOD_NOT_ALLOWED),
None,
),
}
},
|_channel_id, _message_type, _message| {
// client frontend sent a websocket message
// we don't expect this! we only use websockets to push updates
},
);
Ok(())
}
/// On GET: return all active games
fn handle_get(state: &mut ChessState) -> (server::HttpResponse, Option<LazyLoadBlob>) {
(
server::HttpResponse::new(http::StatusCode::OK),
Some(LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::to_vec(&state.games).expect("failed to serialize games!"),
}),
)
}
/// On POST: create a new game
fn handle_post(state: &mut ChessState) -> (server::HttpResponse, Option<LazyLoadBlob>) {
let Some(blob) = get_blob() else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
"Not Found".to_string().as_bytes().to_vec(),
);
return Ok(());
}
match http_request.method()?.as_str() {
// on GET: give the frontend all of our active games
"GET" => Ok(http::send_response(
http::StatusCode::OK,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
serde_json::to_vec(&state.games)?,
)),
// on POST: create a new game
"POST" => {
let Some(blob) = get_blob() else {
return Ok(http::send_response(
http::StatusCode::BAD_REQUEST,
None,
vec![],
));
};
let blob_json = serde_json::from_slice::<serde_json::Value>(&blob.bytes)?;
let Some(game_id) = blob_json["id"].as_str() else {
return Ok(http::send_response(
http::StatusCode::BAD_REQUEST,
None,
vec![],
));
};
if let Some(game) = state.games.get(game_id)
&& !game.ended
{
return Ok(http::send_response(
http::StatusCode::CONFLICT,
None,
vec![],
));
};
let player_white = blob_json["white"]
.as_str()
.unwrap_or(our.node.as_str())
.to_string();
let player_black = blob_json["black"].as_str().unwrap_or(game_id).to_string();
// send the other player a new game request
let Ok(msg) = Request::new()
.target((game_id, our.process.clone()))
.body(serde_json::to_vec(&ChessRequest::NewGame(
NewGameRequest {
white: player_white.clone(),
black: player_black.clone(),
},
))?)
.send_and_await_response(5)?
else {
return Err(anyhow::anyhow!(
"other player did not respond properly to new game request"
));
};
// if they accept, create a new game
// otherwise, should surface error to FE...
if serde_json::from_slice::<ChessResponse>(msg.body())?
!= ChessResponse::NewGameAccepted
{
return Err(anyhow::anyhow!("other player rejected new game request"));
}
// create a new game
let game = Game {
id: game_id.to_string(),
turns: 0,
board: Board::start_pos().fen(),
white: player_white,
black: player_black,
ended: false,
};
let body = serde_json::to_vec(&game)?;
state.games.insert(game_id.to_string(), game);
save_chess_state(&state);
http::send_response(
http::StatusCode::OK,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
body,
);
Ok(())
}
// on PUT: make a move
"PUT" => {
let Some(blob) = get_blob() else {
return Ok(http::send_response(
http::StatusCode::BAD_REQUEST,
None,
vec![],
));
};
let blob_json = serde_json::from_slice::<serde_json::Value>(&blob.bytes)?;
let Some(game_id) = blob_json["id"].as_str() else {
return Ok(http::send_response(
http::StatusCode::BAD_REQUEST,
None,
vec![],
));
};
let Some(game) = state.games.get_mut(game_id) else {
return Ok(http::send_response(
http::StatusCode::NOT_FOUND,
None,
vec![],
));
};
if (game.turns % 2 == 0 && game.white != our.node)
|| (game.turns % 2 == 1 && game.black != our.node)
{
return Ok(http::send_response(
http::StatusCode::FORBIDDEN,
None,
vec![],
));
} else if game.ended {
return Ok(http::send_response(
http::StatusCode::CONFLICT,
None,
vec![],
));
}
let Some(move_str) = blob_json["move"].as_str() else {
return Ok(http::send_response(
http::StatusCode::BAD_REQUEST,
None,
vec![],
));
};
let mut board = Board::from_fen(&game.board).unwrap();
if !board.apply_uci_move(move_str) {
// TODO surface illegal move to player or something here
return Ok(http::send_response(
http::StatusCode::BAD_REQUEST,
None,
vec![],
));
}
// send the move to the other player
// check if the game is over
// if so, update the records
let Ok(msg) = Request::new()
.target((game_id, our.process.clone()))
.body(serde_json::to_vec(&ChessRequest::Move(MoveRequest {
game_id: game_id.to_string(),
move_str: move_str.to_string(),
}))?)
.send_and_await_response(5)?
else {
return Err(anyhow::anyhow!(
"other player did not respond properly to our move"
));
};
if serde_json::from_slice::<ChessResponse>(msg.body())? != ChessResponse::MoveAccepted {
return Err(anyhow::anyhow!("other player rejected our move"));
}
// update the game
game.turns += 1;
if board.checkmate() || board.stalemate() {
game.ended = true;
}
game.board = board.fen();
// update state and return to FE
let body = serde_json::to_vec(&game)?;
save_chess_state(&state);
// return the game
http::send_response(
http::StatusCode::OK,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
body,
);
Ok(())
}
// on DELETE: end the game
"DELETE" => {
let Some(game_id) = http_request.query_params().get("id") else {
return Ok(http::send_response(
http::StatusCode::BAD_REQUEST,
None,
vec![],
));
};
let Some(game) = state.games.get_mut(game_id) else {
return Ok(http::send_response(
http::StatusCode::BAD_REQUEST,
None,
vec![],
));
};
// send the other player an end game request
Request::new()
.target((game_id.as_str(), our.process.clone()))
.body(serde_json::to_vec(&ChessRequest::Resign(our.node.clone()))?)
.send()?;
game.ended = true;
let body = serde_json::to_vec(&game)?;
save_chess_state(&state);
http::send_response(
http::StatusCode::OK,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
body,
);
Ok(())
}
// Any other method will be rejected.
_ => Ok(http::send_response(
http::StatusCode::METHOD_NOT_ALLOWED,
};
let Ok(blob_json) = serde_json::from_slice::<serde_json::Value>(&blob.bytes) else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
vec![],
)),
);
};
let Some(game_id) = blob_json["id"].as_str() else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
let player_white = blob_json["white"]
.as_str()
.unwrap_or(state.our.node.as_str())
.to_string();
let player_black = blob_json["black"].as_str().unwrap_or(game_id).to_string();
match handle_local_request(
state,
&ChessRequest::NewGame(NewGameRequest {
white: player_white,
black: player_black,
}),
) {
Ok(game) => (
server::HttpResponse::new(http::StatusCode::OK)
.header("Content-Type", "application/json"),
Some(LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::to_vec(&game).expect("failed to serialize game!"),
}),
),
Err(e) => (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
Some(LazyLoadBlob {
mime: Some("application/text".to_string()),
bytes: e.to_string().into_bytes(),
}),
),
}
}
/// On PUT: make a move
fn handle_put(state: &mut ChessState) -> (server::HttpResponse, Option<LazyLoadBlob>) {
let Some(blob) = get_blob() else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
let Ok(blob_json) = serde_json::from_slice::<serde_json::Value>(&blob.bytes) else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
let Some(game_id) = blob_json["id"].as_str() else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
let Some(move_str) = blob_json["move"].as_str() else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
match handle_local_request(
state,
&ChessRequest::Move(MoveRequest {
game_id: game_id.to_string(),
move_str: move_str.to_string(),
}),
) {
Ok(game) => (
server::HttpResponse::new(http::StatusCode::OK)
.header("Content-Type", "application/json"),
Some(LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::to_vec(&game).expect("failed to serialize game!"),
}),
),
Err(e) => (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
Some(LazyLoadBlob {
mime: Some("application/text".to_string()),
bytes: e.to_string().into_bytes(),
}),
),
}
}
/// On DELETE: end the game
fn handle_delete(
state: &mut ChessState,
request: &server::IncomingHttpRequest,
) -> (server::HttpResponse, Option<LazyLoadBlob>) {
let Some(game_id) = request.query_params().get("id") else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
match handle_local_request(state, &ChessRequest::Resign(game_id.to_string())) {
Ok(game) => (
server::HttpResponse::new(http::StatusCode::OK)
.header("Content-Type", "application/json"),
Some(LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::to_vec(&game).expect("failed to serialize game!"),
}),
),
Err(e) => (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
Some(LazyLoadBlob {
mime: Some("application/text".to_string()),
bytes: e.to_string().into_bytes(),
}),
),
}
}

View File

@ -10,9 +10,7 @@
"net:distro:sys",
"vfs:distro:sys"
],
"grant_capabilities": [
"http_server:distro:sys"
],
"grant_capabilities": [],
"public": true
}
]

View File

@ -1,15 +1,10 @@
#![feature(let_chains)]
use crate::kinode::process::homepage::{AddRequest, Request as HomepageRequest};
use kinode_process_lib::{
await_message, call_init, get_blob,
http::{
bind_http_path, bind_http_static_path, send_response, serve_ui, HttpServerError,
HttpServerRequest, Method, StatusCode,
},
println, Address, Message,
await_message, call_init, get_blob, http, http::server, println, Address, LazyLoadBlob,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::collections::BTreeMap;
/// Fetching OS version from main package.. LMK if there's a better way...
const CARGO_TOML: &str = include_str!("../../../../Cargo.toml");
@ -45,34 +40,42 @@ call_init!(init);
fn init(our: Address) {
let mut app_data: BTreeMap<String, HomepageApp> = BTreeMap::new();
serve_ui(&our, "ui", true, false, vec!["/"]).expect("failed to serve ui");
let mut http_server = server::HttpServer::new(5);
let http_config = server::HttpBindingConfig::default();
bind_http_static_path(
"/our",
false,
false,
Some("text/html".to_string()),
our.node().into(),
)
.expect("failed to bind to /our");
http_server
.serve_ui(&our, "ui", http_config.clone())
.expect("failed to serve ui");
bind_http_static_path(
"/amionline",
false,
false,
Some("text/html".to_string()),
"yes".into(),
)
.expect("failed to bind to /amionline");
http_server
.bind_http_static_path(
"/our",
false,
false,
Some("text/html".to_string()),
our.node().into(),
)
.expect("failed to bind to /our");
bind_http_static_path(
"/our.js",
false,
false,
Some("application/javascript".to_string()),
format!("window.our = {{}}; window.our.node = '{}';", &our.node).into(),
)
.expect("failed to bind to /our.js");
http_server
.bind_http_static_path(
"/amionline",
false,
false,
Some("text/html".to_string()),
"yes".into(),
)
.expect("failed to bind to /amionline");
http_server
.bind_http_static_path(
"/our.js",
false,
false,
Some("application/javascript".to_string()),
format!("window.our = {{}}; window.our.node = '{}';", &our.node).into(),
)
.expect("failed to bind to /our.js");
// the base version gets written over on-bootstrap, so we look for
// the persisted (user-customized) version first.
@ -99,67 +102,156 @@ fn init(our: Address) {
.write(&stylesheet)
.expect("failed to write to /persisted-kinode.css");
bind_http_static_path(
"/kinode.css",
false, // kinode.css is not auth'd so that apps on subdomains can use it too!
false,
Some("text/css".to_string()),
stylesheet,
)
.expect("failed to bind /kinode.css");
http_server
.bind_http_static_path(
"/kinode.css",
false, // kinode.css is not auth'd so that apps on subdomains can use it too!
false,
Some("text/css".to_string()),
stylesheet,
)
.expect("failed to bind /kinode.css");
bind_http_static_path(
"/kinode.svg",
false, // kinode.svg is not auth'd so that apps on subdomains can use it too!
false,
Some("image/svg+xml".to_string()),
include_str!("../../pkg/kinode.svg").into(),
)
.expect("failed to bind /kinode.svg");
http_server
.bind_http_static_path(
"/kinode.svg",
false, // kinode.svg is not auth'd so that apps on subdomains can use it too!
false,
Some("image/svg+xml".to_string()),
include_str!("../../pkg/kinode.svg").into(),
)
.expect("failed to bind /kinode.svg");
bind_http_static_path(
"/bird-orange.svg",
false, // bird-orange.svg is not auth'd so that apps on subdomains can use it too!
false,
Some("image/svg+xml".to_string()),
include_str!("../../pkg/bird-orange.svg").into(),
)
.expect("failed to bind /bird-orange.svg");
http_server
.bind_http_static_path(
"/bird-orange.svg",
false, // bird-orange.svg is not auth'd so that apps on subdomains can use it too!
false,
Some("image/svg+xml".to_string()),
include_str!("../../pkg/bird-orange.svg").into(),
)
.expect("failed to bind /bird-orange.svg");
bind_http_static_path(
"/bird-plain.svg",
false, // bird-plain.svg is not auth'd so that apps on subdomains can use it too!
false,
Some("image/svg+xml".to_string()),
include_str!("../../pkg/bird-plain.svg").into(),
)
.expect("failed to bind /bird-plain.svg");
http_server
.bind_http_static_path(
"/bird-plain.svg",
false, // bird-plain.svg is not auth'd so that apps on subdomains can use it too!
false,
Some("image/svg+xml".to_string()),
include_str!("../../pkg/bird-plain.svg").into(),
)
.expect("failed to bind /bird-plain.svg");
bind_http_static_path(
"/version",
true,
false,
Some("text/plain".to_string()),
version_from_cargo_toml().into(),
)
.expect("failed to bind /version");
http_server
.bind_http_static_path(
"/version",
true,
false,
Some("text/plain".to_string()),
version_from_cargo_toml().into(),
)
.expect("failed to bind /version");
bind_http_path("/apps", true, false).expect("failed to bind /apps");
bind_http_path("/favorite", true, false).expect("failed to bind /favorite");
bind_http_path("/order", true, false).expect("failed to bind /order");
http_server
.bind_http_path("/apps", http_config.clone())
.expect("failed to bind /apps");
http_server
.bind_http_path("/favorite", http_config.clone())
.expect("failed to bind /favorite");
http_server
.bind_http_path("/order", http_config)
.expect("failed to bind /order");
loop {
let Ok(ref message) = await_message() else {
// we never send requests, so this will never happen
continue;
};
if let Message::Response { source, body, .. } = message
&& source.process == "http_server:distro:sys"
{
match serde_json::from_slice::<Result<(), HttpServerError>>(&body) {
Ok(Ok(())) => continue,
Ok(Err(e)) => println!("got error from http_server: {e}"),
Err(_e) => println!("got malformed message from http_server!"),
if message.source().process == "http_server:distro:sys" {
if message.is_request() {
let Ok(request) = http_server.parse_request(message.body()) else {
continue;
};
http_server.handle_request(
request,
|incoming| {
let path = incoming.bound_path(None);
match path {
"/apps" => (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(
&app_data.values().collect::<Vec<&HomepageApp>>(),
)
.unwrap(),
)),
),
"/favorite" => {
let Ok(http::Method::POST) = incoming.method() else {
return (
server::HttpResponse::new(
http::StatusCode::METHOD_NOT_ALLOWED,
),
None,
);
};
let Some(body) = get_blob() else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
let Ok(favorite_toggle) =
serde_json::from_slice::<(String, bool)>(&body.bytes)
else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
if let Some(app) = app_data.get_mut(&favorite_toggle.0) {
app.favorite = favorite_toggle.1;
}
(server::HttpResponse::new(http::StatusCode::OK), None)
}
"/order" => {
let Ok(http::Method::POST) = incoming.method() else {
return (
server::HttpResponse::new(
http::StatusCode::METHOD_NOT_ALLOWED,
),
None,
);
};
let Some(body) = get_blob() else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
let Ok(order_list) =
serde_json::from_slice::<Vec<(String, u32)>>(&body.bytes)
else {
return (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
None,
);
};
for (app_id, order) in order_list {
if let Some(app) = app_data.get_mut(&app_id) {
app.order = order;
}
}
(server::HttpResponse::new(http::StatusCode::OK), None)
}
_ => (server::HttpResponse::new(http::StatusCode::NOT_FOUND), None),
}
},
|_channel_id, _message_type, _message| {
// not expecting any websocket messages from FE currently
},
);
}
} else {
// handle messages to add or remove an app from the homepage.
@ -210,115 +302,18 @@ fn init(our: Address) {
.write(new_stylesheet_string.as_bytes())
.expect("failed to write to /persisted-kinode.css");
// re-bind
bind_http_static_path(
"/kinode.css",
false, // kinode.css is not auth'd so that apps on subdomains can use it too!
false,
Some("text/css".to_string()),
new_stylesheet_string.into(),
)
.expect("failed to bind /kinode.css");
http_server
.bind_http_static_path(
"/kinode.css",
false, // kinode.css is not auth'd so that apps on subdomains can use it too!
false,
Some("text/css".to_string()),
new_stylesheet_string.into(),
)
.expect("failed to bind /kinode.css");
println!("updated kinode.css!");
}
}
} else if let Ok(req) = serde_json::from_slice::<HttpServerRequest>(message.body()) {
match req {
HttpServerRequest::Http(incoming) => {
let path = incoming.bound_path(None);
match path {
"/apps" => {
send_response(
StatusCode::OK,
Some(HashMap::from([(
"Content-Type".to_string(),
"application/json".to_string(),
)])),
serde_json::to_vec(
&app_data.values().collect::<Vec<&HomepageApp>>(),
)
.unwrap(),
);
}
"/favorite" => {
let Ok(Method::POST) = incoming.method() else {
send_response(
StatusCode::BAD_REQUEST,
Some(HashMap::new()),
vec![],
);
continue;
};
let Some(body) = get_blob() else {
send_response(
StatusCode::BAD_REQUEST,
Some(HashMap::new()),
vec![],
);
continue;
};
let Ok(favorite_toggle) =
serde_json::from_slice::<(String, bool)>(&body.bytes)
else {
send_response(
StatusCode::BAD_REQUEST,
Some(HashMap::new()),
vec![],
);
continue;
};
if let Some(app) = app_data.get_mut(&favorite_toggle.0) {
app.favorite = favorite_toggle.1;
}
send_response(
StatusCode::OK,
Some(HashMap::from([(
"Content-Type".to_string(),
"application/json".to_string(),
)])),
vec![],
);
}
"/order" => {
let Ok(Method::POST) = incoming.method() else {
send_response(
StatusCode::BAD_REQUEST,
Some(HashMap::new()),
vec![],
);
continue;
};
let Some(body) = get_blob() else {
send_response(
StatusCode::BAD_REQUEST,
Some(HashMap::new()),
vec![],
);
continue;
};
let Ok(order_list) =
serde_json::from_slice::<Vec<(String, u32)>>(&body.bytes)
else {
send_response(
StatusCode::BAD_REQUEST,
Some(HashMap::new()),
vec![],
);
continue;
};
for (app_id, order) in order_list {
if let Some(app) = app_data.get_mut(&app_id) {
app.order = order;
}
}
send_response(StatusCode::OK, Some(HashMap::new()), vec![]);
}
_ => {
send_response(StatusCode::NOT_FOUND, Some(HashMap::new()), vec![]);
}
}
}
_ => {}
}
}
}
}

View File

@ -303,15 +303,12 @@ fn handle_log(our: &Address, state: &mut State, log: &eth::Log) -> anyhow::Resul
return Err(anyhow::anyhow!("skipping invalid entry"));
}
println!("got parent hash: {parent_hash}, child hash: {child_hash}, name: {name}");
let full_name = match get_parent_name(&state.names, &parent_hash) {
Some(parent_name) => format!("{name}.{parent_name}"),
None => name,
};
state.names.insert(child_hash.clone(), full_name.clone());
println!("inserted child hash: {child_hash}, with full name: {full_name}");
state.nodes.insert(
full_name.clone(),
net::KnsUpdate {

View File

@ -3,7 +3,7 @@ use kinode_process_lib::{
LazyLoadBlob, Message, NodeId, ProcessId, Request, Response, SendError, SendErrorKind,
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
extern crate base64;
const ICON: &str = include_str!("icon");
@ -42,7 +42,6 @@ enum SettingsError {
#[derive(Debug, Serialize, Deserialize)]
struct SettingsState {
pub our: Address,
pub ws_clients: HashSet<u32>,
pub identity: Option<net::Identity>,
pub diagnostics: Option<String>,
pub eth_rpc_providers: Option<eth::SavedConfigs>,
@ -55,7 +54,6 @@ impl SettingsState {
fn new(our: Address) -> Self {
Self {
our,
ws_clients: HashSet::new(),
identity: None,
diagnostics: None,
eth_rpc_providers: None,
@ -65,17 +63,15 @@ impl SettingsState {
}
}
fn ws_update(&self) {
for channel in &self.ws_clients {
http::send_ws_push(
*channel,
http::WsMessageType::Text,
LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::to_vec(self).unwrap(),
},
);
}
fn ws_update(&self, http_server: &http::server::HttpServer) {
http_server.ws_push_all_channels(
"/",
http::server::WsMessageType::Text,
LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::to_vec(self).unwrap(),
},
)
}
/// get data that the settings page presents to user
@ -182,18 +178,27 @@ fn initialize(our: Address) {
// add ourselves to the homepage
homepage::add_to_homepage("Settings", Some(ICON), Some("/"), None);
// Serve the index.html and other UI files found in pkg/ui at the root path.
// Serving securely at `settings-sys` subdomain
http::secure_serve_ui(&our, "ui", vec!["/"]).unwrap();
http::secure_bind_http_path("/ask").unwrap();
http::secure_bind_ws_path("/", false).unwrap();
// Grab our state, then enter the main event loop.
let mut state: SettingsState = SettingsState::new(our);
main_loop(&mut state);
let mut http_server = http::server::HttpServer::new(5);
// Serve the index.html and other UI files found in pkg/ui at the root path.
// Serving securely at `settings-sys` subdomain
http_server
.serve_ui(
&state.our,
"ui",
http::server::HttpBindingConfig::default().secure_subdomain(true),
)
.unwrap();
http_server.secure_bind_http_path("/ask").unwrap();
http_server.secure_bind_ws_path("/").unwrap();
main_loop(&mut state, &mut http_server);
}
fn main_loop(state: &mut SettingsState) {
fn main_loop(state: &mut SettingsState, http_server: &mut http::server::HttpServer) {
loop {
match await_message() {
Err(send_error) => {
@ -209,7 +214,8 @@ fn main_loop(state: &mut SettingsState) {
if source.node() != state.our.node {
continue; // ignore messages from other nodes
}
let response = handle_request(&source, &body, state);
let response = handle_request(&source, &body, state, http_server);
state.ws_update(http_server);
if expects_response.is_some() {
Response::new()
.body(serde_json::to_vec(&response).unwrap())
@ -222,42 +228,45 @@ fn main_loop(state: &mut SettingsState) {
}
}
fn handle_request(source: &Address, body: &[u8], state: &mut SettingsState) -> SettingsResponse {
fn handle_request(
source: &Address,
body: &[u8],
state: &mut SettingsState,
http_server: &mut http::server::HttpServer,
) -> SettingsResponse {
// source node is ALWAYS ourselves since networking is disabled
if source.process == "http_server:distro:sys" {
// receive HTTP requests and websocket connection messages from our server
match serde_json::from_slice::<http::HttpServerRequest>(body)
.map_err(|_| SettingsError::MalformedRequest)?
{
http::HttpServerRequest::Http(ref incoming) => {
match handle_http_request(state, incoming) {
Ok(()) => Ok(None),
let server_request = http_server
.parse_request(body)
.map_err(|_| SettingsError::MalformedRequest)?;
http_server.handle_request(
server_request,
|req| {
let result = handle_http_request(state, &req);
match result {
Ok((resp, blob)) => (resp, blob),
Err(e) => {
println!("error handling HTTP request: {e}");
http::send_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
None,
"Service Unavailable".to_string().as_bytes().to_vec(),
);
Ok(None)
(
http::server::HttpResponse {
status: 500,
headers: HashMap::new(),
},
Some(LazyLoadBlob {
mime: Some("application/text".to_string()),
bytes: e.to_string().as_bytes().to_vec(),
}),
)
}
}
}
http::HttpServerRequest::WebSocketOpen { channel_id, .. } => {
state.ws_clients.insert(channel_id);
Ok(None)
}
http::HttpServerRequest::WebSocketClose(channel_id) => {
// client frontend closed a websocket
state.ws_clients.remove(&channel_id);
Ok(None)
}
http::HttpServerRequest::WebSocketPush { .. } => {
// client frontend sent a websocket message
// we don't expect this! we only use websockets to push updates
Ok(None)
}
}
},
|_channel_id, _message_type, _blob| {
// we don't expect websocket messages
},
);
Ok(None)
} else {
let settings_request = serde_json::from_slice::<SettingsRequest>(body)
.map_err(|_| SettingsError::MalformedRequest)?;
@ -268,45 +277,46 @@ fn handle_request(source: &Address, body: &[u8], state: &mut SettingsState) -> S
/// Handle HTTP requests from our own frontend.
fn handle_http_request(
state: &mut SettingsState,
http_request: &http::IncomingHttpRequest,
) -> anyhow::Result<()> {
http_request: &http::server::IncomingHttpRequest,
) -> anyhow::Result<(http::server::HttpResponse, Option<LazyLoadBlob>)> {
match http_request.method()?.as_str() {
"GET" => {
state.fetch()?;
Ok(http::send_response(
http::StatusCode::OK,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
serde_json::to_vec(&state)?,
Ok((
http::server::HttpResponse::new(http::StatusCode::OK)
.header("Content-Type", "application/json"),
Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(&state)?,
)),
))
}
"POST" => {
let Some(blob) = get_blob() else {
return Ok(http::send_response(
http::StatusCode::BAD_REQUEST,
None,
vec![],
));
return Err(anyhow::anyhow!("malformed request"));
};
let request = serde_json::from_slice::<SettingsRequest>(&blob.bytes)?;
let response = handle_settings_request(state, request);
Ok(http::send_response(
http::StatusCode::OK,
None,
Ok((
http::server::HttpResponse::new(http::StatusCode::OK)
.header("Content-Type", "application/json"),
match response {
Ok(Some(data)) => serde_json::to_vec(&data)?,
Ok(None) => "null".as_bytes().to_vec(),
Err(e) => serde_json::to_vec(&e)?,
Ok(Some(data)) => Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(&data)?,
)),
Ok(None) => None,
Err(e) => Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(&e)?,
)),
},
))
}
// Any other method will be rejected.
_ => Ok(http::send_response(
http::StatusCode::METHOD_NOT_ALLOWED,
_ => Ok((
http::server::HttpResponse::new(http::StatusCode::METHOD_NOT_ALLOWED),
None,
vec![],
)),
}
}
@ -421,12 +431,10 @@ fn handle_settings_request(
.send()
.unwrap();
state.stylesheet = Some(stylesheet);
state.ws_update();
return SettingsResponse::Ok(None);
}
}
state.fetch().map_err(|_| SettingsError::StateFetchFailed)?;
state.ws_update();
SettingsResponse::Ok(None)
}