aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/cors.rs25
-rw-r--r--src/api/dashboard/dashboard.html35
-rw-r--r--src/api/dashboard/mod.rs15
3 files changed, 60 insertions, 15 deletions
diff --git a/src/api/cors.rs b/src/api/cors.rs
index 406b7be..fc8270d 100644
--- a/src/api/cors.rs
+++ b/src/api/cors.rs
@@ -1,6 +1,6 @@
//! Pages of other origins reading this node: the dashboards of other members, which a
//! page subscribes to for their own links and messages (`/admin/dashboard/links/own`,
-//! `/admin/dashboard/messages/own/events`), refreshing its
+//! `/admin/dashboard/messages/own/events`, and one written out), refreshing its
//! token on any node (`/auth/refresh`, `/auth/whoami`). Only those endpoints, only for
//! the origins of the dashboards the member list names and those of `--cors-origins`,
//! and without credentials: a page sends its token itself (`Authorization: Bearer`),
@@ -18,14 +18,19 @@ use crate::swarm::members;
/// The endpoints other origins may call.
fn open(path: &str) -> bool {
- matches!(
- path,
- "/admin/dashboard/links/own"
- | "/admin/dashboard/links/own/events"
- | "/admin/dashboard/messages/own/events"
- | "/auth/refresh"
- | "/auth/whoami"
- )
+ // A message written out (`/admin/dashboard/messages/own/{seq}`).
+ let message = path
+ .strip_prefix("/admin/dashboard/messages/own/")
+ .is_some_and(|seq| !seq.is_empty() && seq.bytes().all(|byte| byte.is_ascii_digit()));
+ message
+ || matches!(
+ path,
+ "/admin/dashboard/links/own"
+ | "/admin/dashboard/links/own/events"
+ | "/admin/dashboard/messages/own/events"
+ | "/auth/refresh"
+ | "/auth/whoami"
+ )
}
/// `url`'s origin (`scheme://host[:port]`), if it is a URL with one.
@@ -138,6 +143,8 @@ mod tests {
#[test]
fn only_some_endpoints() {
assert!(open("/admin/dashboard/links/own"));
+ assert!(open("/admin/dashboard/messages/own/42"));
+ assert!(!open("/admin/dashboard/messages/own/x"));
assert!(open("/auth/refresh"));
assert!(!open("/auth/login"));
assert!(!open("/store"));
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 9cfb384..6d32d55 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -876,8 +876,9 @@ function renderStats() {
el("span", { class: "key" }, key + ": "), value);
title.replaceChildren("Message: ", what);
retitle(title, title.textContent);
- // A subscribed node's: logged there, the message itself not read from it.
+ // A subscribed node's: logged there, written out as read from it.
if (message.node) {
+ const text = el("pre", { class: "message" }, "reading it from " + name(message.node) + "…");
document.getElementById("stats").replaceChildren(
field("at", clockTime(new Date(message.at))),
field("from", message.from),
@@ -886,7 +887,12 @@ function renderStats() {
...(message.address ? [field("address", message.address)] : []),
field("type", message.type),
field("bytes", String(message.bytes)),
- el("div", { class: "note" }, "Written out only on the node that logged it (its console)."));
+ text);
+ writtenOut(message).then(written => { text.textContent = written; },
+ error => {
+ text.replaceWith(el("div", { class: "note" }, "Not read from " + name(message.node) +
+ ": " + (error.message || String(error))));
+ });
return;
}
document.getElementById("stats").replaceChildren(
@@ -2842,9 +2848,7 @@ function follow(id, delay = 2000) {
const member = state && state.graph.members[id];
if (!member || !member.dashboard) return fail("it publishes no dashboard (--dashboard-url)");
if (!narrowToken) return fail(tokenWhy);
- // Its endpoints, beside its page (`…/admin/dashboard` → `…/admin/dashboard/…`).
- const base = member.dashboard.split("#")[0].replace(/\/+$/, "");
- const at = path => new URL(path, base).href;
+ const at = path => endpointOf(id, path);
let url;
try {
url = at("dashboard/links/own/events");
@@ -2881,6 +2885,25 @@ function follow(id, delay = 2000) {
};
followMessages();
}
+// A member's endpoint, beside the page it publishes (`…/admin/dashboard` →
+// `…/admin/dashboard/<path>`); throws where its address isn't one.
+function endpointOf(id, path) {
+ const member = state && state.graph.members[id];
+ const base = ((member && member.dashboard) || "").split("#")[0].replace(/\/+$/, "");
+ return new URL(path, base).href;
+}
+// A subscribed node's message written out, read from it (its console's
+// `messages/own/{seq}`) with this page's narrow token, kept on the message once read.
+async function writtenOut(message) {
+ if (typeof message.text === "string") return message.text;
+ if (!narrowToken) throw new Error(tokenWhy);
+ const response = await fetch(endpointOf(message.node, "dashboard/messages/own/" + message.ownSeq),
+ { cache: "no-store", headers: { Authorization: "Bearer " + narrowToken.token } });
+ const answer = await response.json().catch(() => ({}));
+ if (!response.ok) throw new Error(response.status + " " + (answer.error || response.statusText));
+ message.text = String(answer.text);
+ return message.text;
+}
// Server-sent events from `url`, read with this page's narrow token (`EventSource`
// can't send one): `opened` once answered, `event(kind, data)` for each; rejects when it
// fails or ends (`offline`: the node stopped).
@@ -3581,7 +3604,7 @@ function remoteMessagesArrived(id, entry, list, sent) {
for (const message of list) {
if (message.way === "refused") continue;
const [from, to] = message.way === "sent" ? [id, message.peer] : [message.peer, id];
- const made = { ...message, seq: id + ":" + message.seq, node: id, from, to,
+ const made = { ...message, seq: id + ":" + message.seq, ownSeq: message.seq, node: id, from, to,
related: message.related ? id + ":" + message.related : null };
if (message.way === "sent") noteDelivery(from, to, message.counted, "sent");
else noteDelivery(from, to, message.counted, "got");
diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs
index 7a643d0..3a50ca6 100644
--- a/src/api/dashboard/mod.rs
+++ b/src/api/dashboard/mod.rs
@@ -509,6 +509,7 @@ pub(crate) fn router(state: AppState) -> Router<AppState> {
"/admin/dashboard/messages/own/events",
get(own_messages_events),
)
+ .route("/admin/dashboard/messages/own/{seq}", get(own_message))
.layer(Extension(Hub::new(state, closing)))
}
@@ -573,6 +574,20 @@ impl Drop for Watching {
}
}
+/// `GET /admin/dashboard/messages/own/{seq}`: the message `seq` of this node's
+/// (`messages/own/events`, which leaves it out) written out, `{"text": …}`, for a page
+/// subscribed to this node showing it; `404` once it isn't kept any more (the latest
+/// 1000, while someone looks).
+async fn own_message(
+ Extension(hub): Extension<Arc<Hub>>,
+ axum::extract::Path(seq): axum::extract::Path<u64>,
+) -> axum::response::Response {
+ match hub.state.traffic.log.text_of(seq) {
+ Some(text) => axum::Json(json!({ "text": text })).into_response(),
+ None => super::error_response(axum::http::StatusCode::NOT_FOUND, "not kept any more", None),
+ }
+}
+
/// `GET /admin/dashboard/messages/own/events`: server-sent events, `messages` with the
/// messages this node sent and received since the last, as they come (gathered for
/// `MESSAGES_GATHERED` after the first), as the page's own `messages` events have them,