mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-11-29 15:24:30 +03:00
update frontend-serving apps to new process_lib http::server utils
This commit is contained in:
parent
6c15904d76
commit
779018b84d
94
Cargo.lock
generated
94
Cargo.lock
generated
@ -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",
|
||||
|
@ -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()));
|
||||
|
@ -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, .. })) =
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,7 @@
|
||||
"net:distro:sys",
|
||||
"vfs:distro:sys"
|
||||
],
|
||||
"grant_capabilities": [
|
||||
"http_server:distro:sys"
|
||||
],
|
||||
"grant_capabilities": [],
|
||||
"public": true
|
||||
}
|
||||
]
|
@ -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![]);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -303,15 +303,12 @@ fn handle_log(our: &Address, state: &mut State, log: ð::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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user