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

View File

@ -1,11 +1,9 @@
use crate::state::{MirrorCheckFile, PackageListing, State}; use crate::state::{MirrorCheckFile, PackageListing, State};
use crate::DownloadResponse; use crate::DownloadResponse;
use kinode_process_lib::{ use kinode_process_lib::{
http::{ http::server,
bind_http_path, bind_ws_path, send_response, serve_ui, IncomingHttpRequest, Method, http::{self, Method, StatusCode},
StatusCode, Address, LazyLoadBlob, NodeId, PackageId, Request,
},
println, Address, NodeId, PackageId, ProcessId, Request,
}; };
use kinode_process_lib::{SendError, SendErrorKind}; use kinode_process_lib::{SendError, SendErrorKind};
use serde_json::json; 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 static and dynamic HTTP paths for the app store,
/// bind to our WS updates path, and add icon and widget to homepage. /// 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 [ for path in [
"/apps", "/apps",
"/apps/:id", "/apps/:id",
@ -27,30 +27,30 @@ pub fn init_frontend(our: &Address) {
"/apps/rebuild-index", "/apps/rebuild-index",
"/mirrorcheck/:node", "/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"); .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 // add ourselves to the homepage
Request::to(("our", "homepage", "homepage", "sys")) kinode_process_lib::homepage::add_to_homepage(
.body( "App Store",
serde_json::json!({ Some(ICON),
"Add": { Some("/"),
"label": "App Store", Some(&make_widget()),
"icon": ICON, );
"path": "/",
"widget": make_widget()
}
})
.to_string()
.as_bytes()
.to_vec(),
)
.send()
.unwrap();
} }
fn make_widget() -> String { fn make_widget() -> String {
@ -183,20 +183,23 @@ fn make_widget() -> String {
/// - stop auto-updating a downloaded app: DELETE /apps/:id/auto-update /// - stop auto-updating a downloaded app: DELETE /apps/:id/auto-update
/// ///
/// - RebuildIndex: POST /apps/rebuild-index /// - 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) { match serve_paths(state, req) {
Ok((status_code, _headers, body)) => send_response( Ok((status_code, _headers, body)) => (
status_code, server::HttpResponse::new(status_code).header("Content-Type", "application/json"),
Some(HashMap::from([( Some(LazyLoadBlob {
String::from("Content-Type"), mime: None,
String::from("application/json"), bytes: body,
)])), }),
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> { 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( fn serve_paths(
state: &mut State, state: &mut State,
req: &IncomingHttpRequest, req: &server::IncomingHttpRequest,
) -> anyhow::Result<(StatusCode, Option<HashMap<String, String>>, Vec<u8>)> { ) -> anyhow::Result<(http::StatusCode, Option<HashMap<String, String>>, Vec<u8>)> {
let method = req.method()?; let method = req.method()?;
let bound_path: &str = req.bound_path(Some(&state.our.process.to_string())); 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, spawn_receive_transfer, spawn_transfer, FTWorkerCommand, FTWorkerResult, FileTransferContext,
}; };
use kinode_process_lib::{ use kinode_process_lib::{
await_message, call_init, eth, get_blob, await_message, call_init, eth, get_blob, http, kimap, println, vfs, Address, LazyLoadBlob,
http::{self, WsMessageType}, Message, PackageId, Request, Response,
kimap, println, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use state::{AppStoreLogError, PackageState, RequestedPackage, State}; use state::{AppStoreLogError, PackageState, RequestedPackage, State};
@ -71,7 +70,7 @@ pub enum Req {
FTWorkerCommand(FTWorkerCommand), FTWorkerCommand(FTWorkerCommand),
FTWorkerResult(FTWorkerResult), FTWorkerResult(FTWorkerResult),
Eth(eth::EthSubResult), Eth(eth::EthSubResult),
Http(http::HttpServerRequest), Http(http::server::HttpServerRequest),
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -80,14 +79,15 @@ pub enum Resp {
LocalResponse(LocalResponse), LocalResponse(LocalResponse),
RemoteResponse(RemoteResponse), RemoteResponse(RemoteResponse),
FTWorkerResult(FTWorkerResult), FTWorkerResult(FTWorkerResult),
HttpClient(Result<http::HttpClientResponse, http::HttpClientError>), HttpClient(Result<http::client::HttpClientResponse, http::client::HttpClientError>),
} }
call_init!(init); call_init!(init);
fn init(our: Address) { fn init(our: Address) {
println!("started"); 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); println!("indexing on contract address {}", KIMAP_ADDRESS);
@ -105,7 +105,7 @@ fn init(our: Address) {
println!("got network error: {send_error}"); println!("got network error: {send_error}");
} }
Ok(message) => { 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); println!("error handling message: {:?}", e);
} }
} }
@ -117,7 +117,11 @@ fn init(our: Address) {
/// function defined for each kind of message. check whether the source /// function defined for each kind of message. check whether the source
/// of the message is allowed to send that kind of message to us. /// of the message is allowed to send that kind of message to us.
/// finally, fire a response if expected from a request. /// 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() { if message.is_request() {
match serde_json::from_slice::<Req>(message.body())? { match serde_json::from_slice::<Req>(message.body())? {
Req::LocalRequest(local_request) => { Req::LocalRequest(local_request) => {
@ -148,23 +152,24 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
total_chunks, total_chunks,
}) => { }) => {
// forward progress to UI // forward progress to UI
let ws_blob = LazyLoadBlob { http_server.ws_push_all_channels(
mime: Some("application/json".to_string()), "/",
bytes: serde_json::json!({ http::server::WsMessageType::Text,
"kind": "progress", LazyLoadBlob {
"data": { mime: Some("application/json".to_string()),
"file_name": file_name, bytes: serde_json::json!({
"chunks_received": chunks_received, "kind": "progress",
"total_chunks": total_chunks, "data": {
} "file_name": file_name,
}) "chunks_received": chunks_received,
.to_string() "total_chunks": total_chunks,
.as_bytes() }
.to_vec(), })
}; .to_string()
for channel_id in state.ui_ws_channels.iter() { .as_bytes()
http::send_ws_push(*channel_id, WsMessageType::Text, ws_blob.clone()); .to_vec(),
} },
);
} }
Req::FTWorkerResult(r) => { Req::FTWorkerResult(r) => {
println!("got weird ft_worker result: {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)); .subscribe_loop(1, utils::app_store_filter(state));
} }
} }
Req::Http(incoming) => { Req::Http(server_request) => {
if !message.is_local(&state.our) if !message.is_local(&state.our)
|| message.source().process != "http_server:distro:sys" || message.source().process != "http_server:distro:sys"
{ {
return Err(anyhow::anyhow!("http_server from non-local node")); return Err(anyhow::anyhow!("http_server from non-local node"));
} }
if let http::HttpServerRequest::Http(req) = incoming { http_server.handle_request(
http_api::handle_http_request(state, &req)?; server_request,
} else if let http::HttpServerRequest::WebSocketOpen { channel_id, .. } = incoming { |incoming| http_api::handle_http_request(state, &incoming),
state.ui_ws_channels.insert(channel_id); |_channel_id, _message_type, _blob| {
} else if let http::HttpServerRequest::WebSocketClose { 0: channel_id } = incoming { // not expecting any websocket messages from FE currently
state.ui_ws_channels.remove(&channel_id); },
} );
} }
} }
} else { } 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(), Some(context) => std::str::from_utf8(context).unwrap_or_default(),
None => return Err(anyhow::anyhow!("http_client response without context")), 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 { if status == 200 {
handle_receive_download(state, &name)?; handle_receive_download(state, &name)?;
@ -435,23 +443,12 @@ pub fn start_download(
}; };
// if `from` is a node, send a request to it // if `from` is a node, send a request to it
// but if it is a url, use http_client to GET 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") { if from.starts_with("http") {
// use http_client to GET it // use http_client to GET it
Request::to(("our", "http_client", "distro", "sys")) http::client::send_request(http::Method::GET, url, None, Some(60), vec![]);
.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();
return DownloadResponse::Started; return DownloadResponse::Started;
} else { } else {
if let Ok(Ok(Message::Response { body, .. })) = if let Ok(Ok(Message::Response { body, .. })) =

View File

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

View File

@ -131,7 +131,7 @@ pub fn fetch_metadata_from_url(
) -> Result<kt::Erc721Metadata, AppStoreLogError> { ) -> Result<kt::Erc721Metadata, AppStoreLogError> {
if let Ok(url) = url::Url::parse(metadata_url) { if let Ok(url) = url::Url::parse(metadata_url) {
if let Ok(_) = 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() { if let Some(body) = get_blob() {
let hash = keccak_256_hash(&body.bytes); let hash = keccak_256_hash(&body.bytes);

View File

@ -1,16 +1,14 @@
#![feature(let_chains)] #![feature(let_chains)]
use crate::kinode::process::chess::{
MoveRequest, NewGameRequest, Request as ChessRequest, Response as ChessResponse,
};
use kinode_process_lib::{ use kinode_process_lib::{
await_message, call_init, get_blob, get_typed_state, http, println, set_state, Address, await_message, call_init, get_blob, get_typed_state, http, http::server, println, set_state,
LazyLoadBlob, Message, NodeId, Request, Response, Address, LazyLoadBlob, Message, NodeId, Request, Response,
}; };
use pleco::Board; use pleco::Board;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet}; 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"); const ICON: &str = include_str!("icon");
@ -30,6 +28,7 @@ struct Game {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct ChessState { struct ChessState {
pub our: Address,
pub games: HashMap<String, Game>, // game is by opposing player id pub games: HashMap<String, Game>, // game is by opposing player id
pub clients: HashSet<u32>, // doesn't get persisted 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()); 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)) { match get_typed_state(|bytes| bincode::deserialize::<HashMap<String, Game>>(bytes)) {
Some(games) => ChessState { Some(games) => ChessState {
our,
games, games,
clients: HashSet::new(), clients: HashSet::new(),
}, },
None => { None => {
let state = ChessState { let state = ChessState {
our,
games: HashMap::new(), games: HashMap::new(),
clients: HashSet::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<()> { fn send_ws_update(http_server: &mut server::HttpServer, game: &Game) {
for channel in open_channels { http_server.ws_push_all_channels(
Request::new() "/",
.target((&our.node, "http_server", "distro", "sys")) server::WsMessageType::Binary,
.body(serde_json::to_vec( LazyLoadBlob {
&http::HttpServerAction::WebSocketPush { mime: Some("application/json".to_string()),
channel_id: *channel, bytes: serde_json::json!({
message_type: http::WsMessageType::Binary, "kind": "game_update",
}, "data": game,
)?)
.blob(LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::json!({
"kind": "game_update",
"data": game,
})
.to_string()
.into_bytes(),
}) })
.send()?; .to_string()
} .into_bytes(),
Ok(()) },
)
} }
// Boilerplate: generate the wasm bindings for a process // Boilerplate: generate the wasm bindings for a process
@ -91,6 +84,7 @@ wit_bindgen::generate!({
generate_unused_types: true, generate_unused_types: true,
additional_derives: [PartialEq, serde::Deserialize, serde::Serialize], additional_derives: [PartialEq, serde::Deserialize, serde::Serialize],
}); });
// After generating bindings, use this macro to define the Component struct // After generating bindings, use this macro to define the Component struct
// and its init() function, which the kernel will look for on startup. // and its init() function, which the kernel will look for on startup.
call_init!(initialize); call_init!(initialize);
@ -99,38 +93,34 @@ fn initialize(our: Address) {
println!("started"); println!("started");
// add ourselves to the homepage // add ourselves to the homepage
Request::to(("our", "homepage", "homepage", "sys")) kinode_process_lib::homepage::add_to_homepage("Chess", Some(ICON), Some("/"), None);
.body(
serde_json::json!({ // create an HTTP server struct with which to manipulate `http_server:distro:sys`
"Add": { let mut http_server = server::HttpServer::new(5);
"label": "Chess", let http_config = server::HttpBindingConfig::default();
"icon": ICON,
"path": "/", // just our root
}
})
.to_string()
.as_bytes()
.to_vec(),
)
.send()
.unwrap();
// Serve the index.html and other UI files found in pkg/ui at the root path. // Serve the index.html and other UI files found in pkg/ui at the root path.
// authenticated=true, local_only=false // 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. // 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). // 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. // Grab our state, then enter the main event loop.
let mut state: ChessState = load_chess_state(); let mut state: ChessState = load_chess_state(our);
main_loop(&our, &mut state); 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 { loop {
// Call await_message() to wait for any incoming messages. // Call await_message() to wait for any incoming messages.
// If we get a network error, make a print and throw it away. // 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:?}"); println!("got network error: {send_error:?}");
continue; continue;
} }
Ok(message) => match handle_request(&our, &message, state) { Ok(message) => match handle_request(&message, state, http_server) {
Ok(()) => continue, 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. /// 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 // Throw away responses. We never expect any responses *here*, because for every
// chess protocol request, we *await* its response in-place. This is appropriate // 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() { if !message.is_request() {
return Ok(()); return Ok(());
} }
// If the request is from another node, handle it as an incoming request. // 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 // 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. // 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 // 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 // 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. // 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 // Deserialize the request IPC to our format, and throw it away if it
// doesn't fit. // doesn't fit.
let Ok(chess_request) = serde_json::from_slice::<ChessRequest>(message.body()) else { let Ok(chess_request) = serde_json::from_slice::<ChessRequest>(message.body()) else {
return Err(anyhow::anyhow!("invalid chess request")); 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! // ...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. // 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 {
} else if message.source().node == our.node // Here, we accept messages *from any local process that can message this one*.
&& message.source().process == "terminal:terminal:sys" // 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 { let Ok(chess_request) = serde_json::from_slice::<ChessRequest>(message.body()) else {
return Err(anyhow::anyhow!("invalid chess request")); return Err(anyhow::anyhow!("invalid chess request"));
}; };
handle_local_request(our, state, &chess_request) let _game = handle_local_request(state, &chess_request)?;
} else if message.source().node == our.node Ok(())
&& 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"
));
} }
} }
/// Handle chess protocol messages from other nodes. /// Handle chess protocol messages from other nodes.
fn handle_chess_request( fn handle_chess_request(
our: &Address,
source_node: &NodeId, source_node: &NodeId,
state: &mut ChessState, state: &mut ChessState,
http_server: &mut server::HttpServer,
action: &ChessRequest, action: &ChessRequest,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
println!("handling action from {source_node}: {action:?}"); 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 // 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 // use a database or something in a real app, and consider performance
// when doing intensive data-based operations. // 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); state.games.insert(game_id.to_string(), game);
save_chess_state(&state); save_chess_state(&state);
// Send a response to tell them we've accepted the game. // Send a response to tell them we've accepted the game.
@ -293,7 +258,7 @@ fn handle_chess_request(
} }
// Persist state. // Persist state.
game.board = board.fen(); game.board = board.fen();
send_ws_update(&our, &game, &state.clients)?; send_ws_update(http_server, &game);
save_chess_state(&state); save_chess_state(&state);
// Send a response to tell them we've accepted the move. // Send a response to tell them we've accepted the move.
Response::new() Response::new()
@ -307,7 +272,7 @@ fn handle_chess_request(
match state.games.get_mut(game_id) { match state.games.get_mut(game_id) {
Some(game) => { Some(game) => {
game.ended = true; game.ended = true;
send_ws_update(&our, &game, &state.clients)?; send_ws_update(http_server, &game);
save_chess_state(&state); save_chess_state(&state);
} }
None => {} None => {}
@ -318,18 +283,18 @@ fn handle_chess_request(
} }
/// Handle actions we are performing. Here's where we'll send_and_await various requests. /// Handle actions we are performing. Here's where we'll send_and_await various requests.
fn handle_local_request( fn handle_local_request(state: &mut ChessState, action: &ChessRequest) -> anyhow::Result<Game> {
our: &Address,
state: &mut ChessState,
action: &ChessRequest,
) -> anyhow::Result<()> {
match action { match action {
ChessRequest::NewGame(NewGameRequest { white, black }) => { ChessRequest::NewGame(NewGameRequest { white, black }) => {
// Create a new game. We'll enforce that one of the two players is us. // 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!")); 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 we already have a game with this player, throw an error.
if let Some(game) = state.games.get(game_id) if let Some(game) = state.games.get(game_id)
&& !game.ended && !game.ended
@ -338,11 +303,11 @@ fn handle_local_request(
}; };
// Send the other player a NewGame request // Send the other player a NewGame request
// The request is exactly the same as what we got from terminal. // 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() 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)?) .body(serde_json::to_vec(&action)?)
.send_and_await_response(5)? .send_and_await_response(30)?
else { else {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"other player did not respond properly to new game request" "other player did not respond properly to new game request"
@ -361,9 +326,9 @@ fn handle_local_request(
black: black.to_string(), black: black.to_string(),
ended: false, ended: false,
}; };
state.games.insert(game_id.to_string(), game); state.games.insert(game_id.to_string(), game.clone());
save_chess_state(&state); save_chess_state(&state);
Ok(()) Ok(game)
} }
ChessRequest::Move(MoveRequest { game_id, move_str }) => { ChessRequest::Move(MoveRequest { game_id, move_str }) => {
// Make a move. We'll enforce that it's our turn. The game_id is the // 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 { let Some(game) = state.games.get_mut(game_id) else {
return Err(anyhow::anyhow!("no game with {game_id}")); return Err(anyhow::anyhow!("no game with {game_id}"));
}; };
if (game.turns % 2 == 0 && game.white != our.node) if (game.turns % 2 == 0 && game.white != state.our.node)
|| (game.turns % 2 == 1 && game.black != our.node) || (game.turns % 2 == 1 && game.black != state.our.node)
{ {
return Err(anyhow::anyhow!("not our turn!")); return Err(anyhow::anyhow!("not our turn!"));
} else if game.ended { } 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. // 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. // 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() 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)?) .body(serde_json::to_vec(&action)?)
.send_and_await_response(5)? .send_and_await_response(30)?
else { else {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"other player did not respond properly to our move" "other player did not respond properly to our move"
@ -402,8 +367,9 @@ fn handle_local_request(
game.ended = true; game.ended = true;
} }
game.board = board.fen(); game.board = board.fen();
let game = game.clone();
save_chess_state(&state); save_chess_state(&state);
Ok(()) Ok(game)
} }
ChessRequest::Resign(ref with_who) => { ChessRequest::Resign(ref with_who) => {
// Resign from a game with a given player. // 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 // send the other player an end game request -- no response expected
Request::new() Request::new()
.target((with_who, our.process.clone())) .target((with_who, state.our.process.clone()))
.body(serde_json::to_vec(&action)?) .body(serde_json::to_vec(&action)?)
.send()?; .send()?;
game.ended = true; game.ended = true;
let game = game.clone();
save_chess_state(&state); save_chess_state(&state);
Ok(()) Ok(game)
} }
} }
} }
/// Handle HTTP requests from our own frontend. /// Handle HTTP requests from our own frontend.
fn handle_http_request( fn handle_http_request(
our: &Address,
state: &mut ChessState, state: &mut ChessState,
http_request: &http::IncomingHttpRequest, http_server: &mut server::HttpServer,
message: &Message,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if http_request.bound_path(Some(&our.process.to_string())) != "/games" { let request = http_server.parse_request(message.body())?;
http::send_response(
http::StatusCode::NOT_FOUND, // 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, None,
"Not Found".to_string().as_bytes().to_vec(),
); );
return Ok(()); };
} let Ok(blob_json) = serde_json::from_slice::<serde_json::Value>(&blob.bytes) else {
match http_request.method()?.as_str() { return (
// on GET: give the frontend all of our active games server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
"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,
None, 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", "net:distro:sys",
"vfs:distro:sys" "vfs:distro:sys"
], ],
"grant_capabilities": [ "grant_capabilities": [],
"http_server:distro:sys"
],
"public": true "public": true
} }
] ]

View File

@ -1,15 +1,10 @@
#![feature(let_chains)] #![feature(let_chains)]
use crate::kinode::process::homepage::{AddRequest, Request as HomepageRequest}; use crate::kinode::process::homepage::{AddRequest, Request as HomepageRequest};
use kinode_process_lib::{ use kinode_process_lib::{
await_message, call_init, get_blob, await_message, call_init, get_blob, http, http::server, println, Address, LazyLoadBlob,
http::{
bind_http_path, bind_http_static_path, send_response, serve_ui, HttpServerError,
HttpServerRequest, Method, StatusCode,
},
println, Address, Message,
}; };
use serde::{Deserialize, Serialize}; 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... /// Fetching OS version from main package.. LMK if there's a better way...
const CARGO_TOML: &str = include_str!("../../../../Cargo.toml"); const CARGO_TOML: &str = include_str!("../../../../Cargo.toml");
@ -45,34 +40,42 @@ call_init!(init);
fn init(our: Address) { fn init(our: Address) {
let mut app_data: BTreeMap<String, HomepageApp> = BTreeMap::new(); 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( http_server
"/our", .serve_ui(&our, "ui", http_config.clone())
false, .expect("failed to serve ui");
false,
Some("text/html".to_string()),
our.node().into(),
)
.expect("failed to bind to /our");
bind_http_static_path( http_server
"/amionline", .bind_http_static_path(
false, "/our",
false, false,
Some("text/html".to_string()), false,
"yes".into(), Some("text/html".to_string()),
) our.node().into(),
.expect("failed to bind to /amionline"); )
.expect("failed to bind to /our");
bind_http_static_path( http_server
"/our.js", .bind_http_static_path(
false, "/amionline",
false, false,
Some("application/javascript".to_string()), false,
format!("window.our = {{}}; window.our.node = '{}';", &our.node).into(), Some("text/html".to_string()),
) "yes".into(),
.expect("failed to bind to /our.js"); )
.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 base version gets written over on-bootstrap, so we look for
// the persisted (user-customized) version first. // the persisted (user-customized) version first.
@ -99,67 +102,156 @@ fn init(our: Address) {
.write(&stylesheet) .write(&stylesheet)
.expect("failed to write to /persisted-kinode.css"); .expect("failed to write to /persisted-kinode.css");
bind_http_static_path( http_server
"/kinode.css", .bind_http_static_path(
false, // kinode.css is not auth'd so that apps on subdomains can use it too! "/kinode.css",
false, false, // kinode.css is not auth'd so that apps on subdomains can use it too!
Some("text/css".to_string()), false,
stylesheet, Some("text/css".to_string()),
) stylesheet,
.expect("failed to bind /kinode.css"); )
.expect("failed to bind /kinode.css");
bind_http_static_path( http_server
"/kinode.svg", .bind_http_static_path(
false, // kinode.svg is not auth'd so that apps on subdomains can use it too! "/kinode.svg",
false, false, // kinode.svg is not auth'd so that apps on subdomains can use it too!
Some("image/svg+xml".to_string()), false,
include_str!("../../pkg/kinode.svg").into(), Some("image/svg+xml".to_string()),
) include_str!("../../pkg/kinode.svg").into(),
.expect("failed to bind /kinode.svg"); )
.expect("failed to bind /kinode.svg");
bind_http_static_path( http_server
"/bird-orange.svg", .bind_http_static_path(
false, // bird-orange.svg is not auth'd so that apps on subdomains can use it too! "/bird-orange.svg",
false, false, // bird-orange.svg is not auth'd so that apps on subdomains can use it too!
Some("image/svg+xml".to_string()), false,
include_str!("../../pkg/bird-orange.svg").into(), Some("image/svg+xml".to_string()),
) include_str!("../../pkg/bird-orange.svg").into(),
.expect("failed to bind /bird-orange.svg"); )
.expect("failed to bind /bird-orange.svg");
bind_http_static_path( http_server
"/bird-plain.svg", .bind_http_static_path(
false, // bird-plain.svg is not auth'd so that apps on subdomains can use it too! "/bird-plain.svg",
false, false, // bird-plain.svg is not auth'd so that apps on subdomains can use it too!
Some("image/svg+xml".to_string()), false,
include_str!("../../pkg/bird-plain.svg").into(), Some("image/svg+xml".to_string()),
) include_str!("../../pkg/bird-plain.svg").into(),
.expect("failed to bind /bird-plain.svg"); )
.expect("failed to bind /bird-plain.svg");
bind_http_static_path( http_server
"/version", .bind_http_static_path(
true, "/version",
false, true,
Some("text/plain".to_string()), false,
version_from_cargo_toml().into(), Some("text/plain".to_string()),
) version_from_cargo_toml().into(),
.expect("failed to bind /version"); )
.expect("failed to bind /version");
bind_http_path("/apps", true, false).expect("failed to bind /apps"); http_server
bind_http_path("/favorite", true, false).expect("failed to bind /favorite"); .bind_http_path("/apps", http_config.clone())
bind_http_path("/order", true, false).expect("failed to bind /order"); .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 { loop {
let Ok(ref message) = await_message() else { let Ok(ref message) = await_message() else {
// we never send requests, so this will never happen // we never send requests, so this will never happen
continue; continue;
}; };
if let Message::Response { source, body, .. } = message if message.source().process == "http_server:distro:sys" {
&& source.process == "http_server:distro:sys" if message.is_request() {
{ let Ok(request) = http_server.parse_request(message.body()) else {
match serde_json::from_slice::<Result<(), HttpServerError>>(&body) { continue;
Ok(Ok(())) => continue, };
Ok(Err(e)) => println!("got error from http_server: {e}"), http_server.handle_request(
Err(_e) => println!("got malformed message from http_server!"), 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 { } else {
// handle messages to add or remove an app from the homepage. // 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()) .write(new_stylesheet_string.as_bytes())
.expect("failed to write to /persisted-kinode.css"); .expect("failed to write to /persisted-kinode.css");
// re-bind // re-bind
bind_http_static_path( http_server
"/kinode.css", .bind_http_static_path(
false, // kinode.css is not auth'd so that apps on subdomains can use it too! "/kinode.css",
false, false, // kinode.css is not auth'd so that apps on subdomains can use it too!
Some("text/css".to_string()), false,
new_stylesheet_string.into(), Some("text/css".to_string()),
) new_stylesheet_string.into(),
.expect("failed to bind /kinode.css"); )
.expect("failed to bind /kinode.css");
println!("updated 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")); 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) { let full_name = match get_parent_name(&state.names, &parent_hash) {
Some(parent_name) => format!("{name}.{parent_name}"), Some(parent_name) => format!("{name}.{parent_name}"),
None => name, None => name,
}; };
state.names.insert(child_hash.clone(), full_name.clone()); state.names.insert(child_hash.clone(), full_name.clone());
println!("inserted child hash: {child_hash}, with full name: {full_name}");
state.nodes.insert( state.nodes.insert(
full_name.clone(), full_name.clone(),
net::KnsUpdate { net::KnsUpdate {

View File

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