From 8615f66d64fc29cf17a4b0b7a51056f408ae6386 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Sat, 14 Sep 2024 20:11:01 +0300 Subject: [PATCH 1/7] 0.9.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- data/resolver.3.bin | Bin 0 -> 3320 bytes src/config.rs | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 data/resolver.3.bin diff --git a/Cargo.lock b/Cargo.lock index c1f4076..1e53f7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2268,7 +2268,7 @@ dependencies = [ [[package]] name = "kaspa-resolver" -version = "0.8.0" +version = "0.9.0" dependencies = [ "ahash", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index b3fd47b..438b44f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "kaspa-resolver" description = "Kaspa RPC endpoint resolver" -version = "0.8.0" +version = "0.9.0" edition = "2021" # authors.workspace = true # include.workspace = true diff --git a/data/resolver.3.bin b/data/resolver.3.bin new file mode 100644 index 0000000000000000000000000000000000000000..c90d00d8a7cfee6b5962d9c7317313d35b74ef80 GIT binary patch literal 3320 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 Date: Sat, 14 Sep 2024 22:02:40 +0300 Subject: [PATCH 2/7] public status page --- src/args.rs | 9 +++ src/cache.rs | 39 ++++++++++ src/connection.rs | 85 ---------------------- src/imports.rs | 4 +- src/main.rs | 2 + src/public.rs | 104 +++++++++++++++++++++++++++ src/resolver.rs | 67 ++++------------- src/status.rs | 119 +++++++++++++++++++++--------- templates/public.css | 91 +++++++++++++++++++++++ templates/public.html | 54 ++++++++++++++ templates/public.js | 164 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 565 insertions(+), 173 deletions(-) create mode 100644 src/cache.rs create mode 100644 src/public.rs create mode 100644 templates/public.css create mode 100644 templates/public.html create mode 100644 templates/public.js diff --git a/src/args.rs b/src/args.rs index 05b7687..67c7d90 100644 --- a/src/args.rs +++ b/src/args.rs @@ -30,6 +30,8 @@ pub struct Args { pub auto_update: bool, /// Custom config file pub user_config: Option, + /// public status page + pub public: bool, // Show node data on each election // pub election: bool, // Enable resolver status access via `/status` @@ -39,6 +41,10 @@ pub struct Args { } impl Args { + pub fn public(&self) -> bool { + self.public + } + pub fn parse() -> Args { #[allow(unused)] use clap::{arg, command, Arg, Command}; @@ -49,6 +55,7 @@ impl Args { )) .arg(arg!(--version "Display software version")) .arg(arg!(--verbose "Enable verbose logging")) + .arg(arg!(--public "Enable public status page")) .arg(arg!(--trace "Enable trace log level")) .arg(arg!(--debug "Enable additional debug output")) // .arg(arg!(--auto-update "Poll configuration updates")) @@ -94,6 +101,7 @@ impl Args { let matches = cmd.get_matches(); + let public = matches.get_one::("public").cloned().unwrap_or(false); let trace = matches.get_one::("trace").cloned().unwrap_or(false); let verbose = matches.get_one::("verbose").cloned().unwrap_or(false); let debug = matches.get_one::("debug").cloned().unwrap_or(false); @@ -173,6 +181,7 @@ impl Args { debug, auto_update, user_config, + public, // election, // status, listen, diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..f1e354c --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,39 @@ +use axum::{ + body::Body, + http::{header, HeaderValue}, + response::{IntoResponse, Response}, +}; + +#[derive(Clone, Copy, Debug)] +#[must_use] +pub struct NoCacheHtml(pub T); + +impl IntoResponse for NoCacheHtml +where + T: Into, +{ + fn into_response(self) -> Response { + ( + [ + ( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()), + ), + ( + header::CACHE_CONTROL, + HeaderValue::from_static( + "no-cache, no-store, must-revalidate, proxy-revalidate, max-age=0", + ), + ), + ], + self.0.into(), + ) + .into_response() + } +} + +impl From for NoCacheHtml { + fn from(inner: T) -> Self { + Self(inner) + } +} diff --git a/src/connection.rs b/src/connection.rs index d80d10d..ecf7a35 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -461,88 +461,3 @@ impl<'a> From<&'a Arc> for Output<'a> { } } } - -#[derive(Serialize)] -pub struct Status<'a> { - pub version: String, - #[serde(with = "SerHex::")] - pub sid: u64, - #[serde(with = "SerHex::")] - pub uid: u64, - pub url: &'a str, - pub fqdn: &'a str, - pub service: String, - // pub service: &'a str, - pub protocol: ProtocolKind, - pub encoding: EncodingKind, - pub encryption: TlsKind, - pub network: &'a NetworkId, - pub cores: u64, - pub memory: u64, - pub status: &'static str, - pub peers: u64, - pub clients: u64, - pub capacity: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub delegates: Option>, -} - -impl<'a> From<&'a Arc> for Status<'a> { - fn from(connection: &'a Arc) -> Self { - let delegate = connection.delegate(); - - let node = connection.node(); - let uid = node.uid(); - let url = node.address.as_str(); - let fqdn = node.fqdn.as_str(); - let service = node.service().to_string(); - let protocol = node.params().protocol(); - let encoding = node.params().encoding(); - let encryption = node.params().tls(); - let network = &node.network; - let status = connection.status(); - let clients = delegate.clients(); - let peers = delegate.peers(); - let (version, sid, capacity, cores, memory) = delegate - .caps() - .as_ref() - .as_ref() - .map(|caps| { - ( - caps.version.clone(), - caps.system_id, - caps.clients_limit, - caps.cpu_physical_cores, - caps.total_memory, - ) - }) - .unwrap_or_else(|| ("n/a".to_string(), 0, 0, 0, 0)); - - let delegates = connection - .resolve_delegators() - .iter() - .map(|connection| format!("[{:016x}] {}", connection.system_id(), connection.address())) - .collect::>(); - let delegates = (!delegates.is_empty()).then_some(delegates); - - Self { - sid, - uid, - version, - fqdn, - service, - url, - protocol, - encoding, - encryption, - network, - cores, - memory, - status, - clients, - peers, - capacity, - delegates, - } - } -} diff --git a/src/imports.rs b/src/imports.rs index 2a6d05a..3884c9d 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -1,6 +1,7 @@ pub use crate::args::Args; +pub use crate::cache::NoCacheHtml; pub use crate::config::*; -pub use crate::connection::{Connection, Output, Status}; +pub use crate::connection::{Connection, Output}; pub use crate::delegate::*; pub use crate::error::Error; pub use crate::events::Events; @@ -10,6 +11,7 @@ pub use crate::monitor::Monitor; pub use crate::node::*; pub use crate::params::PathParams; pub use crate::path::*; +pub(crate) use crate::public; pub use crate::resolver::Resolver; pub use crate::result::Result; pub(crate) use crate::rpc; diff --git a/src/main.rs b/src/main.rs index e61400b..9b2bc57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod args; +mod cache; mod config; mod connection; mod delegate; @@ -12,6 +13,7 @@ mod node; mod panic; mod params; mod path; +mod public; mod resolver; mod result; mod rpc; diff --git a/src/public.rs b/src/public.rs new file mode 100644 index 0000000..3b159f1 --- /dev/null +++ b/src/public.rs @@ -0,0 +1,104 @@ +use crate::imports::*; +use askama::Template; + +use axum::{ + body::Body, + http::{header, HeaderValue, Request, StatusCode}, + response::{IntoResponse, Response}, +}; + +#[derive(Template)] +#[template(path = "public.html", escape = "none")] +struct PublicTemplate {} + +pub async fn json_handler(resolver: &Arc, _req: Request) -> impl IntoResponse { + let connections = resolver.connections(); + let connections = connections + .iter() + // .filter(|c| c.is_delegate()) + .map(Public::from) + .collect::>(); + let nodes = serde_json::to_string(&connections).unwrap(); + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "application/json") + .header( + header::CACHE_CONTROL, + HeaderValue::from_static( + "no-cache, no-store, must-revalidate, proxy-revalidate, max-age=0", + ), + ) + .body(Body::from(nodes)) + .unwrap() +} + +pub async fn status_handler(_resolver: &Arc, _req: Request) -> impl IntoResponse { + let index = PublicTemplate {}; + + Response::builder() + .status(StatusCode::OK) + .header( + header::CACHE_CONTROL, + HeaderValue::from_static( + "no-cache, no-store, must-revalidate, proxy-revalidate, max-age=0", + ), + ) + .body(Body::from(index.render().unwrap())) + .unwrap() +} + +#[derive(Serialize)] +pub struct Public<'a> { + pub version: String, + #[serde(with = "SerHex::")] + pub sid: u64, + #[serde(with = "SerHex::")] + pub uid: u64, + pub service: String, + pub protocol: ProtocolKind, + pub encoding: EncodingKind, + pub encryption: TlsKind, + pub network: &'a NetworkId, + pub status: &'static str, + pub peers: u64, + pub clients: u64, + pub capacity: u64, +} + +impl<'a> From<&'a Arc> for Public<'a> { + fn from(connection: &'a Arc) -> Self { + let delegate = connection.delegate(); + + let node = connection.node(); + let uid = node.uid(); + let service = node.service().to_string(); + let protocol = node.params().protocol(); + let encoding = node.params().encoding(); + let encryption = node.params().tls(); + let network = &node.network; + let status = connection.status(); + let clients = delegate.clients(); + let peers = delegate.peers(); + let (version, sid, capacity) = delegate + .caps() + .as_ref() + .as_ref() + .map(|caps| (caps.version.clone(), caps.system_id, caps.clients_limit)) + .unwrap_or_else(|| ("n/a".to_string(), 0, 0)); + + Self { + sid, + uid, + version, + service, + protocol, + encoding, + encryption, + network, + status, + clients, + peers, + capacity, + } + } +} diff --git a/src/resolver.rs b/src/resolver.rs index a6ac6cc..706ad42 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -98,6 +98,20 @@ impl Resolver { get(|req: Request| async move { status::json_handler(&this, req).await }), ); + if self.args().public() { + let this = self.clone(); + router = router.route( + "/", + get(|req: Request| async move { public::status_handler(&this, req).await }), + ); + + let this = self.clone(); + router = router.route( + "/json", + get(|req: Request| async move { public::json_handler(&this, req).await }), + ); + } + if let Some(rate_limit) = self.args().rate_limit.as_ref() { log_success!( "Limits", @@ -278,34 +292,6 @@ impl Resolver { } } - // respond with a JSON object containing the status of all nodes - #[allow(dead_code)] - fn get_status(&self, monitor: Option<&Monitor>, filter: F) -> impl IntoResponse - where - F: Fn(&&Arc) -> bool, - { - if let Some(monitor) = monitor { - let connections = monitor.to_vec(); - let status = connections - .iter() - .filter(filter) - .map(Status::from) - .collect::>(); - - with_json(status) - } else { - let kaspa = self.inner.kaspa.to_vec(); - let sparkle = self.inner.sparkle.to_vec(); - let status = kaspa - .iter() - .chain(sparkle.iter()) - .filter(filter) - .map(Status::from) - .collect::>(); - with_json(status) - }; - } - // // respond with a JSON object containing the status of all nodes pub fn connections(&self) -> Vec> { let kaspa = self.inner.kaspa.to_vec(); @@ -374,31 +360,6 @@ fn with_json_string(json: String) -> Response { .into_response() } -#[inline] -fn with_json(data: T) -> Response -where - T: Serialize, -{ - ( - StatusCode::OK, - [ - ( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), - ), - ( - header::CACHE_CONTROL, - HeaderValue::from_static( - "no-cache, no-store, must-revalidate, proxy-revalidate, max-age=0", - ), - ), - (header::CONNECTION, HeaderValue::from_static("close")), - ], - serde_json::to_string(&data).unwrap(), - ) - .into_response() -} - #[inline] #[allow(dead_code)] fn with_mime(body: impl Into, mime: &'static str) -> Response { diff --git a/src/status.rs b/src/status.rs index ef3639e..88efa3a 100644 --- a/src/status.rs +++ b/src/status.rs @@ -13,40 +13,6 @@ pub enum RequestKind { Post(Form>), } -#[derive(Clone, Copy, Debug)] -#[must_use] -pub struct NoCacheHtml(pub T); - -impl IntoResponse for NoCacheHtml -where - T: Into, -{ - fn into_response(self) -> Response { - ( - [ - ( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()), - ), - ( - header::CACHE_CONTROL, - HeaderValue::from_static( - "no-cache, no-store, must-revalidate, proxy-revalidate, max-age=0", - ), - ), - ], - self.0.into(), - ) - .into_response() - } -} - -impl From for NoCacheHtml { - fn from(inner: T) -> Self { - Self(inner) - } -} - #[derive(Template)] #[template(path = "index.html", escape = "none")] struct IndexTemplate { @@ -194,3 +160,88 @@ pub async fn status_handler(resolver: &Arc, req: RequestKind) -> impl } } } + +#[derive(Serialize)] +pub struct Status<'a> { + pub version: String, + #[serde(with = "SerHex::")] + pub sid: u64, + #[serde(with = "SerHex::")] + pub uid: u64, + pub url: &'a str, + pub fqdn: &'a str, + pub service: String, + // pub service: &'a str, + pub protocol: ProtocolKind, + pub encoding: EncodingKind, + pub encryption: TlsKind, + pub network: &'a NetworkId, + pub cores: u64, + pub memory: u64, + pub status: &'static str, + pub peers: u64, + pub clients: u64, + pub capacity: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub delegates: Option>, +} + +impl<'a> From<&'a Arc> for Status<'a> { + fn from(connection: &'a Arc) -> Self { + let delegate = connection.delegate(); + + let node = connection.node(); + let uid = node.uid(); + let url = node.address.as_str(); + let fqdn = node.fqdn.as_str(); + let service = node.service().to_string(); + let protocol = node.params().protocol(); + let encoding = node.params().encoding(); + let encryption = node.params().tls(); + let network = &node.network; + let status = connection.status(); + let clients = delegate.clients(); + let peers = delegate.peers(); + let (version, sid, capacity, cores, memory) = delegate + .caps() + .as_ref() + .as_ref() + .map(|caps| { + ( + caps.version.clone(), + caps.system_id, + caps.clients_limit, + caps.cpu_physical_cores, + caps.total_memory, + ) + }) + .unwrap_or_else(|| ("n/a".to_string(), 0, 0, 0, 0)); + + let delegates = connection + .resolve_delegators() + .iter() + .map(|connection| format!("[{:016x}] {}", connection.system_id(), connection.address())) + .collect::>(); + let delegates = (!delegates.is_empty()).then_some(delegates); + + Self { + sid, + uid, + version, + fqdn, + service, + url, + protocol, + encoding, + encryption, + network, + cores, + memory, + status, + clients, + peers, + capacity, + delegates, + } + } +} diff --git a/templates/public.css b/templates/public.css new file mode 100644 index 0000000..b7a2f23 --- /dev/null +++ b/templates/public.css @@ -0,0 +1,91 @@ + +code { + display: block; + font-size: inherit; + font-family: inherit; + white-space: pre-wrap; +} + + +span.number { + color: rgb(11, 103, 20); +} + +span.key { + color: rgb(22, 32, 110); +} + +span.string { + color: rgb(16, 115, 105); +} + +span.boolean { + color: rgb(91, 9, 0); +} + +span.keyword { + color: rgb(2, 83, 67); +} + +.network { + color: rgb(11, 103, 20); +} + +a, .link { + color: rgb(0, 156, 117); + text-decoration: none; +} + +a:hover, .link:hover { + color: rgb(80, 121, 111); + text-decoration: none; + cursor: pointer; +} +.link:disabled{ + opacity:0.5; + cursor:not-allowed +} + + +input::placeholder { + font-size: 14px; + font-family: "Menlo", "Consolas", "Andale Mono", monospace; +} + +tr.online > td { + color: rgb(11, 103, 20); +} + +tr.offline > td { + color: rgb(91, 9, 0); +} + +tr.delegator > td { + color: rgb(22, 32, 110); +} + +tr.syncing > td { + color: rgb(149, 116, 37); +} + +th { + text-align: left; + font-size: 14px; + font-weight: lighter; + color: rgb(58, 58, 58); + padding: 2px 4px; +} + +td { + color: rgb(44, 44, 44); + padding: 2px 4px; +} + +.right { + text-align: right; +} + +.wide { + padding-left : 16px; + padding-right: 8px; +} \ No newline at end of file diff --git a/templates/public.html b/templates/public.html new file mode 100644 index 0000000..10c4fb5 --- /dev/null +++ b/templates/public.html @@ -0,0 +1,54 @@ + + + + Status + + + + + + +
+ + + +
+
+ + diff --git a/templates/public.js b/templates/public.js new file mode 100644 index 0000000..8e34592 --- /dev/null +++ b/templates/public.js @@ -0,0 +1,164 @@ +document.addEventListener('DOMContentLoaded', () => { + window.resolver = { + sort : "network", + nodes : [], + networks : { + "mainnet": true, + "testnet-10": true, + "testnet-11": true, + }, + }; + + table = document.createElement('table'); + document.body.appendChild(table); + + thead = document.createElement('thead'); + table.appendChild(thead); + thead.innerHTML = "SID:UIDSERVICEVERSIONNETWORKSTATUSPEERSCLIENTS/CAPLOAD"; + + tbody = document.createElement('tbody'); + tbody.id = "nodes"; + table.appendChild(tbody); + + fetchData(); +}); + +function fetchData() { + fetch('/json') + .then(response => response.json()) + .then(data => { + window.resolver.nodes = data; + render(); + setTimeout(fetchData, 7500); + }) + .catch(error => { + setTimeout(fetchData, 1000); + console.error('Error fetching data:', error); + }); +} + +function filter(node, ctx) { + if (!window.resolver.networks[node.network]) { + return "hidden"; + } else if (node.status == "offline" && !ctx.offline) { + return "hidden"; + } else if (node.status == "delegator" && !ctx.delegators) { + return "hidden"; + } else { + return node.status; + } +} + +function render() { + + let ctx = { + offline : true, + delegators : false, + }; + + let resort = false; + let tbody = document.getElementById("nodes"); + + const status = window.resolver.nodes + .filter((node) => node.status === 'online') + .reduce((acc, node) => { + const network = node.network; + if (!acc[network]) { + acc[network] = { clients: 0, capacity: 0, count : 0 }; + } + acc[network].clients += node.clients; + acc[network].capacity += node.capacity; + acc[network].count += 1; + return acc; + }, {}); + + if (window.resolver.sort != window.resolver.lastSort) { + resort = true; + window.resolver.lastSort = window.resolver.sort; + } + + window.resolver.nodes.forEach((node) => { + let { + version, + sid, + uid, + service, + url, + protocol, + encoding, + encryption, + network, + cores, + memory, + status, + peers, + clients, + capacity, + delegates, + } = node; + + let el = document.getElementById(uid); + if (!el) { + el = document.createElement('tr'); + el.id = uid; + el.setAttribute('data-sort', node.network); + tbody.appendChild(el); + resort = true; + } + el.className = filter(node, ctx); + + let load = (clients / capacity * 100.0).toFixed(2); + let peers_ = peers.toLocaleString(); + let clients_ = clients.toLocaleString(); + let capacity_ = capacity.toLocaleString(); + el.innerHTML = `${sid}:${uid}${service}${version}${network}${status}`; + if (status != "offline") { + el.innerHTML += `${peers_}${clients_} / ${capacity_}${load}%`; + } + }); + + if (resort) { + sort(); + } + + let status_entries = Object.entries(status); + status_entries.sort(([a], [b]) => a.localeCompare(b)); + status_entries.forEach(([network, status]) => { + let el = document.getElementById(`${network}-data`); + if (!el) { + let tbody = document.getElementById("status"); + el = document.createElement('td'); + el.id = network; + el.innerHTML = `    `; + tbody.appendChild(el); + + if (window.resolver.networks[network] == undefined) { + window.resolver.networks[network] = true; + } + + document.getElementById(`${network}-filter`).addEventListener('change', () => { + window.resolver.networks[network] = document.getElementById(`${network}-filter`).checked; + render(); + }); + el = document.getElementById(`${network}-data`); + } + let load = (status.clients / status.capacity * 100.0).toFixed(2); + let count = status.count.toLocaleString(); + let clients = status.clients.toLocaleString(); + let capacity = status.capacity.toLocaleString(); + el.innerHTML = `(${count}) ${clients} / ${capacity}  ${load}%`; + }); + +} + +function sort() { + let tbody = document.getElementById("nodes"); + let rows = Array.from(tbody.getElementsByTagName('tr')); + rows.sort((a, b) => { + let aValue = a.getAttribute('data-sort'); + let bValue = b.getAttribute('data-sort'); + return aValue.localeCompare(bValue); + }); + tbody.innerHTML = ''; // Clear existing rows + rows.forEach(row => tbody.appendChild(row)); +} From edc1911849a55f3614293f95ccf8acda21ed851a Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Sat, 14 Sep 2024 22:11:49 +0300 Subject: [PATCH 3/7] add version to status page --- src/status.rs | 7 ++++--- templates/index.html | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/status.rs b/src/status.rs index 88efa3a..f452b86 100644 --- a/src/status.rs +++ b/src/status.rs @@ -17,6 +17,7 @@ pub enum RequestKind { #[template(path = "index.html", escape = "none")] struct IndexTemplate { access: bool, + version : &'static str, } pub async fn logout_handler(resolver: &Arc, req: Request) -> impl IntoResponse { @@ -119,7 +120,7 @@ pub async fn status_handler(resolver: &Arc, req: RequestKind) -> impl Ok((Some(session), cookie)) => { session.touch(); - let index = IndexTemplate { access: true }; + let index = IndexTemplate { access: true, version : crate::VERSION }; if let Some(cookie) = cookie { Response::builder() @@ -151,11 +152,11 @@ pub async fn status_handler(resolver: &Arc, req: RequestKind) -> impl .body(Body::from(msg)) .unwrap(), Err(Error::Unauthorized) => { - let index = IndexTemplate { access: false }; + let index = IndexTemplate { access: false, version : crate::VERSION }; NoCacheHtml(index.render().unwrap()).into_response() } _ => { - let index = IndexTemplate { access: false }; + let index = IndexTemplate { access: false, version : crate::VERSION }; NoCacheHtml(index.render().unwrap()).into_response() } } diff --git a/templates/index.html b/templates/index.html index f13e817..480d7fc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -61,6 +61,8 @@