Fix issues found in review of host pairing

- Saves of a datastore run one at a time, and changes made while saving
  are saved by the next save. Overlapping saves from approving and
  revoking hosts could write an older snapshot after a newer one.
- Map subscribers fetch everything again when a new session starts, as
  the controller only sends values that exist. A host revoked while a
  browser was disconnected stayed in its list.
- The host token check accepts the same pre-rename tokens as the
  WebSocket, so upgrading a host with such a token does not make it ask
  to join again.
This commit is contained in:
Danielv123 committed 2026-10-30 21:49:53 +01:00
1 parent 2cf7c66187
commit 1ed46dff9c
8 files changed
+121 -30

No files matched your search

+24
View File
@@ -1025,6 +1025,30 @@ export default class Controller {
return jwt.sign({ aud: "host", host: hostId }, this.authSecret);
}
/**
* Check a host token
*
* @param token - Host token to check.
* @returns ID of the host the token was issued to.
* @throws if the token is invalid, its host is not registered or it was issued before tokenValidAfter.
*/
verifyHostToken(token: string): number {
// migrate: allow pre-rename tokens issued to hosts before alpha-14
const payload = jwt.verify(token, this.authSecret, { audience: ["host", "slave"] });
if (typeof payload === "string") {
throw new Error("unexpected JsonWebToken type");
}
const hostId = payload.host !== undefined ? payload.host : payload.slave;
const host = this.hosts.get(hostId);
if (!host) {
throw new Error("host is not registered, start the host again to request to join the cluster");
}
if (!payload.iat || payload.iat < host.tokenValidAfter) {
throw new Error("invalid token");
}
return hostId;
}
hostsUpdated(hosts: HostRecord[]) {
let updates = hosts.map(host => host.toHostDetails());
this.subscriptions.broadcast(new lib.HostUpdatesEvent(updates));
+1 -16
View File
@@ -252,24 +252,9 @@ ${err.stack}`
}
try {
// migrate: allow pre-rename tokens issued to hosts before alpha-14
const tokenPayload = jwt.verify(data.token, this.controller.authSecret, { audience: ["host", "slave"] });
if (typeof tokenPayload === "string") {
throw new Error("unexpected JsonWebToken type");
}
// migrate: allow pre-rename tokens issued to hosts before alpha-14
if ((tokenPayload.host !== undefined ? tokenPayload.host : tokenPayload.slave) !== data.id) {
if (this.controller.verifyHostToken(data.token) !== data.id) {
throw new Error("missmatched host id");
}
let host = this.controller.hosts.get(data.id);
if (!host) {
throw new Error("host is not registered, start the host again to request to join the cluster");
}
if (!tokenPayload.iat || tokenPayload.iat < host.tokenValidAfter) {
throw new Error("invalid token");
}
} catch (err: any) {
logger.verbose(`WsServer | authentication failed for ${this.remoteAddr(req)}`);
wsRejectedConnectionsCounter.inc();
+1 -11
View File
@@ -189,17 +189,7 @@ function validateHostToken(req: Request, res: Response, next: any) {
}
try {
const tokenPayload = jwt.verify(token, req.app.locals.controller.authSecret, { audience: "host" });
if (typeof tokenPayload === "string") {
throw new Error("unexpected JsonWebToken type");
}
let host = req.app.locals.controller.hosts.get(tokenPayload.host);
if (!host) {
throw new Error("invalid host");
}
if (!tokenPayload.iat || tokenPayload.iat < (host.tokenValidAfter ?? 0)) {
throw new Error("invalid token");
}
req.app.locals.controller.verifyHostToken(token);
} catch (err) {
res.sendStatus(401);
return;