Files
MikrotikManager/backend/src/services/bandwidth-peer-metadata-source.ts
T

108 lines
7.3 KiB
TypeScript

/** Fixed managed metadata comparison. This is NOT actor, Q or native authority.
* The trusted caller must check its genuine current original grant first.
* Passwords are private bound predicates, never result projections or hashes. */
import { types } from "node:util"
import { performance } from "node:perf_hooks"
import type { PoolClient, QueryConfig } from "pg"
import type { BandwidthPeerSourceSnapshot } from "./bandwidth-peer-observation.js"
import type { BandwidthNodeObservationScope } from "./bandwidth-node-observation.js"
export class BandwidthPeerMetadataSourceRefused extends Error {
readonly code = "BANDWIDTH_PEER_METADATA_SOURCE_REFUSED"
constructor() { super("Bandwidth peer metadata refused") }
}
const refuse = (): never => { throw new BandwidthPeerMetadataSourceRefused() }
const checkers = new WeakSet<object>()
function data(raw: unknown, names: readonly string[]): Record<string, unknown> {
if (!raw || typeof raw !== "object" || types.isProxy(raw) || Object.getPrototypeOf(raw) !== Object.prototype) return refuse()
const fields = Object.getOwnPropertyDescriptors(raw)
if (Reflect.ownKeys(raw).length !== names.length || names.some(name => !fields[name])
|| Object.values(fields).some(field => !("value" in field) || !field.enumerable)) return refuse()
return Object.fromEntries(names.map(name => [name, fields[name].value]))
}
const bounded = (raw: unknown, max: number): raw is string => typeof raw === "string" && Buffer.byteLength(raw) <= max
const SQL = `/* bandwidth-managed:metadata-current */ SELECT EXISTS (
SELECT 1 FROM public.servers
WHERE id=$1::bigint AND enabled IS TRUE AND host=$2::text AND port=$3::integer
AND use_ssl=$4::boolean AND verify_ssl=$5::boolean
AND username=$6::text AND password=$7::text
AND routing_endpoint_epoch=$8::uuid
) AS metadata_current`
export function assertManagedBandwidthPeerMetadataChecker(value: unknown): asserts value is ReturnType<typeof createManagedBandwidthPeerMetadataChecker> {
if (!value || typeof value !== "object" || !checkers.has(value)) refuse()
}
export function createManagedBandwidthPeerMetadataChecker() {
const checker = Object.freeze({
async assertMetadataCurrent(expected: Readonly<BandwidthPeerSourceSnapshot>, rawScope: BandwidthNodeObservationScope): Promise<void> {
const wall = Date.now(), mono = performance.now()
let client: PoolClient | undefined, active = true, released = false, success = false
let releaseFailed = false, socketFailed = false
let timer: ReturnType<typeof setTimeout> | undefined, signal: AbortSignal | undefined, cancel: (() => void) | undefined
const onError = () => { socketFailed = true; if (active) cancel?.() }
const release = (destroy: boolean) => {
if (!client || released) return
released = true
try {
client.release(destroy)
if (!destroy) client.removeListener("error", onError)
} catch { releaseFailed = true }
// A destroyed/uncertain client retains its listener as a late-error
// absorber. Never retry an uncertain release or expose its raw error.
}
try {
const original = data(expected, ["peerRef", "enabled", "sourceProof", "authorityGeneration", "endpoint"])
// External source has no persistent current metadata provider. Never
// invent a server alias, consult a supplied snapshot again or fall back.
const peer = data(original.peerRef, ["kind", "serverId"])
const endpoint = data(original.endpoint, ["host", "port", "useSsl", "verifySsl", "username", "password", "routingEndpointEpoch"])
if (peer.kind !== "server" || !Number.isSafeInteger(peer.serverId) || Number(peer.serverId) < 1 || original.enabled !== true
|| !bounded(original.sourceProof, 64) || !/^[a-f0-9]{64}$/.test(original.sourceProof)
|| !bounded(original.authorityGeneration, 200) || !original.authorityGeneration
|| !bounded(endpoint.host, 1000) || !endpoint.host || /[\s/\\\u0000-\u001f\u007f]/.test(endpoint.host)
|| !Number.isSafeInteger(endpoint.port) || Number(endpoint.port) < 1 || Number(endpoint.port) > 65535
|| typeof endpoint.useSsl !== "boolean" || typeof endpoint.verifySsl !== "boolean"
|| !bounded(endpoint.username, 1000) || !bounded(endpoint.password, 65536)
|| !bounded(endpoint.routingEndpointEpoch, 36) || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(endpoint.routingEndpointEpoch)) refuse()
if (!rawScope || typeof rawScope !== "object" || types.isProxy(rawScope) || Object.getPrototypeOf(rawScope) !== Object.prototype) refuse()
const fields = data(rawScope, Object.hasOwn(rawScope, "signal") ? ["deadlineAt", "signal"] : ["deadlineAt"])
if (!Number.isSafeInteger(fields.deadlineAt) || fields.signal !== undefined && (types.isProxy(fields.signal) || !(fields.signal instanceof AbortSignal))) refuse()
signal = fields.signal as AbortSignal | undefined
const deadline = Math.min(Number(fields.deadlineAt), wall + 5000), end = mono + Math.max(0, deadline - wall)
const aborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted")!.get!
const live = () => { const now = Date.now(); if (!active || now < wall || now >= deadline || performance.now() >= end || signal && Reflect.apply(aborted, signal, [])) refuse() }
const values = [peer.serverId, endpoint.host, endpoint.port, endpoint.useSsl, endpoint.verifySsl, endpoint.username, endpoint.password, endpoint.routingEndpointEpoch]
let rejectStopped!: (error: BandwidthPeerMetadataSourceRefused) => void
const stopped = new Promise<never>((_resolve, reject) => { rejectStopped = reject }); void stopped.catch(() => {})
cancel = () => { active = false; release(true); rejectStopped(new BandwidthPeerMetadataSourceRefused()) }
live(); timer = setTimeout(cancel, Math.max(1, Math.ceil(end - performance.now())))
if (signal) Reflect.apply(EventTarget.prototype.addEventListener, signal, ["abort", cancel, { once: true }])
await Promise.race([stopped, (async () => {
live(); const { pool } = await import("../db/index.js"); live()
const acquired = await pool.connect()
if (!active) {
acquired.on("error", () => {})
try { acquired.release(true) } catch { /* Original call already refused. */ }
return refuse()
}
client = acquired; client.on("error", onError); live()
const config: QueryConfig & { query_timeout: number } = { text: SQL, values, query_timeout: Math.max(1, Math.ceil(Math.min(deadline - Date.now(), end - performance.now()))) }
const result = await client.query(config)
live()
if (result.command !== "SELECT" || result.rowCount !== 1 || result.rows.length !== 1) refuse()
const row = data(result.rows[0], ["metadata_current"])
if (row.metadata_current !== true) refuse()
live(); success = true
})()]); live()
} catch { throw new BandwidthPeerMetadataSourceRefused() }
finally {
active = false; clearTimeout(timer)
if (signal && cancel) Reflect.apply(EventTarget.prototype.removeEventListener, signal, ["abort", cancel])
release(!success)
if (releaseFailed || socketFailed) throw new BandwidthPeerMetadataSourceRefused()
}
},
})
checkers.add(checker); return checker
}