tyo-mq
Version:
Distributed Message Pub/Sub Service with socket.io
1,253 lines (1,111 loc) • 151 kB
JavaScript
/**
* Messaging Server
*/
'esversion: 6';
const crypto = require('crypto');
const fs = require('fs');
const http = require('http');
const https = require('https');
const path = require('path');
const pak = require('../package.json');
const env = require('./env');
const adminSignature = require('./admin-signature');
const attachRemoteNamespace = require('./remote-namespace');
const ClusterSync = require('./cluster');
// info
var eventManager = require('./events');
const Constants = require('./constants');
const Logger = require('./logger');
const Settings = require('./settings');
const Storage = require('./storage');
function Server(options) {
this.options = options || {};
// Hot-loadable settings store — seeded with constructor options.
// Call server.loadSettings(path) to watch a JSON file for live updates.
this.settings = new Settings(this.options);
// Convenience alias kept for backwards compatibility; always reads live value.
Object.defineProperty(this, 'authOptions', {
get: function () { return this.settings.get('auth') || {}; },
enumerable: true
});
var server = this;
var remoteNsp = null;
var remoteIo = null;
this.logger = new Logger('tyo-mq', { level: Logger.LOG });
this.store = Storage.createStore(this.options);
Object.defineProperty(this, 'remote', {
get: function () { return remoteNsp; },
enumerable: true
});
var app = http.createServer((req, res) => {
if (!handleHttpApiRequest(req, res)) {
res.writeHead(403);
res.end();
}
});
// Default to 50 MB so large messages don't silently drop the connection.
// Callers can override by passing maxHttpBufferSize in options.
var ioOptions = Object.assign({ maxHttpBufferSize: 50 * 1024 * 1024 }, options);
var io = require('socket.io')(app, ioOptions);
// app.listen(this.options.port || Constants.DEFAULT_PORT);
var DEFAULT_REALM = 'default';
var REALM_ALL = '*';
var realms = {};
var authorizationRequests = [];
var authorizationRequestMap = {};
var authorizationRequestClientMap = {};
var usedManagerNonces = new Set();
var loadedSettingsFile = null;
var applyingPersistenceChange = false;
var clusterSync = null;
var applyingRemoteSettings = false;
Object.defineProperty(this, 'cluster', {
get: function () { return clusterSync; },
enumerable: true
});
var getEnvPath = function () {
return server.authOptions.env_file || process.env.TYO_MQ_ENV_FILE || '.env';
};
var getAdminTokenEnvName = function () {
return server.authOptions.admin_token_env || 'TYO_MQ_ADMIN_TOKEN';
};
var generateToken = function () {
return crypto.randomBytes(32).toString('hex');
};
var ensureRemoteNamespace = function (socketIo) {
if (remoteNsp && remoteIo === socketIo)
return remoteNsp;
remoteIo = socketIo;
remoteNsp = attachRemoteNamespace(socketIo, server.options.remote || {});
return remoteNsp;
};
var hasAdminToken = function (auth) {
var tokens = (auth && auth.tokens) || [];
for (var i = 0; i < tokens.length; i++) {
if (tokens[i].token && tokens[i].realm === REALM_ALL && tokens[i].role === 'admin')
return true;
}
return false;
};
var getAdminTokens = function () {
var tokens = (server.authOptions && server.authOptions.tokens) || [];
return tokens.filter(function (item) {
return item && item.token && item.realm === REALM_ALL && item.role === 'admin';
}).map(function (item) {
return item.token;
});
};
var validateManagerProofEnvelope = function (proof) {
if (!proof || !proof.nonce)
return {ok: false, code: 401, message: 'Missing manager proof'};
if (usedManagerNonces.has(proof.nonce))
return {ok: false, code: 401, message: 'Manager proof nonce was already used'};
return {ok: true};
};
var verifyProofWithSecrets = function (secrets, action, body, proof) {
for (var i = 0; i < secrets.length; i++) {
if (secrets[i] && adminSignature.verifyAdminProof(secrets[i], action, body || {}, proof))
return true;
}
return false;
};
var markManagerProofUsed = function (proof) {
usedManagerNonces.add(proof.nonce);
};
var getRealmConfig = function (realm) {
var auth = server.authOptions || {};
var authRealms = auth.realms || {};
return authRealms[realm] || null;
};
// A name is "anonymous" when it is the bare ANONYMOUS placeholder or a
// client-minted unique ANONYMOUS-<uuid> identity (no app_id was configured).
var isAnonymousName = function (name) {
return name === Constants.ANONYMOUS
|| (typeof name === 'string' && name.indexOf(Constants.ANONYMOUS + '-') === 0);
};
// Whether unnamed (ANONYMOUS / no app_id) producers and consumers may
// register in a realm. An explicit realms.<id>.allow_anonymous wins;
// otherwise only the open 'default' realm permits anonymous — configured
// and named realms reject it unless they opt in.
var isAnonymousAllowed = function (realmId) {
var rc = getRealmConfig(realmId);
if (rc) {
if (typeof rc.allow_anonymous === 'boolean') return rc.allow_anonymous;
if (typeof rc.allowAnonymous === 'boolean') return rc.allowAnonymous;
}
return realmId === DEFAULT_REALM;
};
var getRealmManagerKey = function (realm) {
var realmConfig = getRealmConfig(realm);
if (!realmConfig)
return null;
return realmConfig.manager_key || realmConfig.managerKey || null;
};
var verifyGlobalManagerProof = function (action, body, proof) {
var envelope = validateManagerProofEnvelope(proof);
if (!envelope.ok)
return envelope;
if (verifyProofWithSecrets(getAdminTokens(), action, body, proof)) {
markManagerProofUsed(proof);
return {ok: true, scope: 'global'};
}
return {ok: false, code: 401, message: 'Invalid manager proof'};
};
var verifyAuthorizationNextProof = function (body, proof) {
body = body || {};
var envelope = validateManagerProofEnvelope(proof);
if (!envelope.ok)
return envelope;
if (verifyProofWithSecrets(getAdminTokens(), 'AUTHORIZATION_NEXT', body, proof)) {
markManagerProofUsed(proof);
return {ok: true, scope: 'global'};
}
if (!body.realm)
return {ok: false, code: 401, message: 'Invalid manager proof'};
var managerKey = getRealmManagerKey(body.realm);
if (managerKey && adminSignature.verifyAdminProof(managerKey, 'AUTHORIZATION_NEXT', body, proof)) {
markManagerProofUsed(proof);
return {ok: true, scope: 'realm', realm: body.realm};
}
return {ok: false, code: 401, message: 'Invalid manager proof'};
};
var verifyAuthorizationDecisionProof = function (body, proof, request) {
body = body || {};
var envelope = validateManagerProofEnvelope(proof);
if (!envelope.ok)
return envelope;
if (verifyProofWithSecrets(getAdminTokens(), 'AUTHORIZATION_DECIDE', body, proof)) {
markManagerProofUsed(proof);
return {ok: true, scope: 'global'};
}
if (!request || request.status !== 'pending')
return {ok: false, code: 401, message: 'Invalid manager proof'};
var managerKey = getRealmManagerKey(request.realm);
if (managerKey && adminSignature.verifyAdminProof(managerKey, 'AUTHORIZATION_DECIDE', body, proof)) {
markManagerProofUsed(proof);
return {ok: true, scope: 'realm', realm: request.realm};
}
return {ok: false, code: 401, message: 'Invalid manager proof'};
};
var ensureAdminToken = function () {
var auth = server.authOptions || {};
if (!auth.enabled || hasAdminToken(auth))
return;
var envFile = getEnvPath();
var tokenEnv = getAdminTokenEnvName();
env.loadEnvFile(envFile);
var token = process.env[tokenEnv];
if (!token) {
if (auth.auto_admin_token === false)
return;
token = generateToken();
env.appendEnvValue(envFile, tokenEnv, token);
if (server.logger)
server.logger.warn("Generated admin auth token and saved it to " + envFile + " as " + tokenEnv);
}
var nextAuth = Object.assign({}, auth, {
tokens: (auth.tokens || []).concat([{token: token, realm: REALM_ALL, role: 'admin'}])
});
server.settings.merge({auth: nextAuth});
};
var hashToken = function (token) {
return crypto.createHash('sha256').update(String(token)).digest('hex');
};
var publicAuthorizationRequest = function (request) {
if (!request)
return null;
return {
request_id: request.request_id,
status: request.status,
realm: request.realm,
role: request.role,
client_id: request.client_id,
client_name: request.client_name,
client_token_hash: request.client_token_hash,
challenge_response: request.challenge_response,
created_at: request.created_at,
decided_at: request.decided_at || null,
decision_reason: request.decision_reason || null
};
};
var addRuntimeAuthToken = function (token, realm, role, meta) {
var auth = server.authOptions || {};
var tokens = auth.tokens || [];
for (var i = 0; i < tokens.length; i++) {
if (tokens[i].token === token)
return;
}
var entry = Object.assign({}, meta || {}, {
token: token,
realm: realm,
role: normalizeRole(role)
});
server.settings.merge({
auth: Object.assign({}, auth, {tokens: tokens.concat([entry])})
});
};
var clone = function (obj) {
return JSON.parse(JSON.stringify(obj || {}));
};
var createStoreFromSettings = function (settings) {
settings = settings || {};
var storageOptions = Object.assign({}, settings.storage_options || settings.storageOptions || {});
var storage = settings.storage || settings.store || 'memory';
if (storage === 'custom') {
var modulePath = storageOptions.module || storageOptions.module_path || settings.storage_module;
if (!modulePath)
throw new Error('custom storage requires storage_options.module');
storage = require(path.isAbsolute(modulePath) ? modulePath : path.resolve(process.cwd(), modulePath));
}
return Storage.createStore({
storage: storage,
storage_options: storageOptions
});
};
var publicPersistenceSettings = function () {
var allSettings = server.settings.get() || {};
var storage = allSettings.storage || allSettings.store || 'memory';
var storageOptions = clone(allSettings.storage_options || allSettings.storageOptions || {});
if (storage && typeof storage !== 'string')
storage = 'custom';
return {
storage: storage,
storage_options: storageOptions
};
};
var persistenceSignature = function (settings) {
settings = settings || {};
return JSON.stringify({
storage: settings.storage || settings.store || 'memory',
storage_options: settings.storage_options || settings.storageOptions || {}
});
};
var publicManagementSettings = function () {
var auth = publicAuthSettings();
auth.persistence = publicPersistenceSettings();
return auth;
};
var normalizePersistenceSettings = function (body) {
var storage = String(body.storage || body.backend || 'memory').toLowerCase();
var allowed = {memory: true, sqlite: true, redis: true, custom: true};
if (!allowed[storage])
return {ok: false, code: 400, message: 'unsupported storage backend'};
var storageOptions = Object.assign({}, body.storage_options || body.storageOptions || body.options || {});
if (body.default_ttl !== undefined && body.default_ttl !== '')
storageOptions.default_ttl = Number(body.default_ttl);
if (!Number.isFinite(Number(storageOptions.default_ttl)) && storageOptions.default_ttl !== undefined)
return {ok: false, code: 400, message: 'default_ttl must be a number'};
if (storage === 'sqlite') {
if (body.filename)
storageOptions.filename = body.filename;
if (body.path)
storageOptions.filename = body.path;
}
if (storage === 'redis') {
if (body.url)
storageOptions.url = body.url;
if (body.prefix)
storageOptions.prefix = body.prefix;
}
if (storage === 'custom') {
if (body.module)
storageOptions.module = body.module;
if (!storageOptions.module && !storageOptions.module_path)
return {ok: false, code: 400, message: 'custom storage requires a module path'};
}
return {
ok: true,
settings: {
storage: storage,
storage_options: storageOptions
}
};
};
var applyPersistenceSettings = function (persistence) {
var nextStore = createStoreFromSettings(persistence);
var previousStore = server.store;
server.store = nextStore;
if (previousStore && previousStore !== nextStore && typeof previousStore.close === 'function') {
try {
var closeResult = previousStore.close();
if (closeResult && typeof closeResult.catch === 'function')
closeResult.catch(function (err) {
server.logger.error("Previous storage close failed: " + err.message);
});
}
catch (err) {
server.logger.error("Previous storage close failed: " + err.message);
}
}
};
var revokeAuthTokens = function (auth, body) {
var token = body.token || null;
var tokenHash = body.token_hash || body.tokenHash || null;
var realm = body.realm || null;
var clientId = body.client_id || body.clientId || null;
var allowAdmin = !!body.allow_admin;
if (!token && !tokenHash && !(realm && clientId))
return {ok: false, code: 400, message: 'token, token_hash, or realm + client_id is required'};
var removed = [];
var kept = [];
auth.tokens.forEach(function (entry) {
var matches = false;
if (token && entry.token === token)
matches = true;
if (tokenHash && entry.token && hashToken(entry.token) === tokenHash)
matches = true;
if (realm && clientId && entry.realm === realm && entry.client_id === clientId)
matches = true;
if (matches) {
if (entry.realm === REALM_ALL && entry.role === 'admin' && !allowAdmin) {
kept.push(entry);
return;
}
removed.push(entry);
return;
}
kept.push(entry);
});
if (removed.length === 0)
return {ok: false, code: 404, message: 'matching revocable token not found'};
auth.tokens = kept;
return {
ok: true,
revoked: removed.map(function (entry) {
return {
realm: entry.realm,
role: entry.role,
client_id: entry.client_id || null,
client_name: entry.client_name || null,
token_hash: entry.token ? hashToken(entry.token) : null
};
})
};
};
var publicAuthSettings = function () {
var auth = clone(server.authOptions || {});
auth.tokens = (auth.tokens || []).map(function (token) {
return {
realm: token.realm,
role: token.role,
client_id: token.client_id || null,
client_name: token.client_name || null,
token_hash: token.token ? hashToken(token.token) : null
};
});
if (auth.realms) {
Object.keys(auth.realms).forEach(function (realm) {
var realmConfig = auth.realms[realm] || {};
if (realmConfig.manager_key || realmConfig.managerKey) {
delete realmConfig.manager_key;
delete realmConfig.managerKey;
realmConfig.manager_key_configured = true;
}
if (realmConfig.key || realmConfig.preshared_key || realmConfig.presharedKey) {
delete realmConfig.key;
delete realmConfig.preshared_key;
delete realmConfig.presharedKey;
realmConfig.key_configured = true;
}
});
}
if (auth.jwt_secret)
auth.jwt_secret = '<configured>';
if (auth.auth_secret)
auth.auth_secret = '<configured>';
if (auth.management_tokens || auth.managementTokens) {
auth.management_tokens = (auth.management_tokens || auth.managementTokens || []).map(function (entry) {
entry = entry || {};
return {
realm_prefix: entry.realm_prefix || entry.realmPrefix || null,
token_hash: entry.token ? hashToken(entry.token) : null
};
});
delete auth.managementTokens;
}
return auth;
};
var writeSettingsFile = function () {
if (!loadedSettingsFile)
return;
var dir = path.dirname(loadedSettingsFile);
if (!fs.existsSync(dir))
fs.mkdirSync(dir, {recursive: true});
fs.writeFileSync(loadedSettingsFile, JSON.stringify(server.settings.get(), null, 2) + '\n');
};
var persistSettings = function () {
writeSettingsFile();
if (clusterSync && !applyingRemoteSettings) {
clusterSync.publishSettings(server.settings.get()).catch(function (err) {
server.logger.error("Cluster settings publish failed: " + err.message);
});
}
};
// A peer published new settings: adopt them as the source of truth and
// refresh the local file cache, without re-publishing (echo guard).
var applyRemoteSettings = function (remoteSettings, revision) {
applyingRemoteSettings = true;
try {
server.settings.replace(remoteSettings);
}
finally {
applyingRemoteSettings = false;
}
writeSettingsFile();
server.logger.info("Cluster settings applied" + (revision !== undefined ? " (revision " + revision + ")" : ""));
};
var initCluster = function () {
if (clusterSync)
return;
// The raw constructor options keep injected redis clients intact;
// the settings store (JSON-cloned) covers file-based configuration.
var config = server.options.cluster || server.settings.get('cluster');
if (!config || !config.enabled)
return;
var allSettings = server.settings.get() || {};
var storageOptions = allSettings.storage_options || allSettings.storageOptions || {};
try {
clusterSync = new ClusterSync({
prefix: config.prefix || config.channel_prefix || config.channelPrefix,
redis_url: config.redis_url || config.redisUrl || config.url || storageOptions.url,
client: config.client,
subscriber: config.subscriber,
node_id: config.node_id || config.nodeId,
logger: server.logger
});
}
catch (err) {
server.logger.error("Cluster sync init failed: " + err.message);
clusterSync = null;
return;
}
clusterSync.ready = clusterSync.connect().then(function () {
return clusterSync.onSettingsChange(function (settings, revision) {
applyRemoteSettings(settings, revision);
});
}).then(function () {
return clusterSync.fetchSettings();
}).then(function (doc) {
if (doc && doc.settings)
applyRemoteSettings(doc.settings);
else
return clusterSync.publishSettings(server.settings.get());
}).then(function () {
server.logger.log("Cluster sync active (node " + clusterSync.nodeId + ")");
}).catch(function (err) {
server.logger.error("Cluster sync startup failed: " + err.message);
});
};
var NONCE_CLAIM_TTL_MS = 10 * 60 * 1000; // 2x the proof timestamp window
// Cluster-wide replay protection: a manager proof nonce may be used on
// exactly one node. Resolves true when this node won the claim (or no
// cluster is configured — the local nonce set still protects the node).
var claimClusterNonce = function (proof) {
if (!clusterSync || !proof || !proof.nonce)
return Promise.resolve(true);
return clusterSync.claimNonce(proof.nonce, NONCE_CLAIM_TTL_MS).catch(function (err) {
server.logger.error("Cluster nonce claim failed: " + err.message);
return true; // fail open: redis outage must not lock out managers
});
};
// ── shared authorization requests ─────────────────────────────────────
// In a cluster, pending authorization requests live in Redis so a manager
// can poll and decide them from any node. The local in-memory maps keep
// working unchanged for single-node deployments.
var mirrorAuthRequestToCluster = function (request) {
if (!clusterSync)
return;
clusterSync.getAuthRequests().then(function (doc) {
Object.keys(doc).forEach(function (id) {
var existing = doc[id];
if (existing.status === 'pending' && existing.realm === request.realm
&& existing.client_id === request.client_id) {
existing.status = 'superseded';
existing.superseded_at = request.created_at;
}
});
doc[request.request_id] = request;
return clusterSync.putAuthRequests(doc);
}).catch(function (err) {
server.logger.error("Cluster auth request mirror failed: " + err.message);
});
};
var findNextAuthorizationRequest = function (filter) {
if (!clusterSync)
return Promise.resolve(nextPendingAuthorizationRequest(filter));
return clusterSync.getAuthRequests().then(function (doc) {
var pending = Object.keys(doc).map(function (id) { return doc[id]; })
.filter(function (request) {
if (request.status !== 'pending') return false;
if (filter && filter.realm && request.realm !== filter.realm) return false;
return true;
})
.sort(function (a, b) { return String(a.created_at).localeCompare(String(b.created_at)); });
return pending[0] || null;
}).catch(function (err) {
server.logger.error("Cluster auth request lookup failed: " + err.message);
return nextPendingAuthorizationRequest(filter);
});
};
var lookupAuthorizationRequest = function (requestId) {
var local = authorizationRequestMap[requestId];
if (!clusterSync)
return Promise.resolve(local);
return clusterSync.getAuthRequests().then(function (doc) {
return doc[requestId] || local;
}).catch(function () {
return local;
});
};
var persistAuthDecisionToCluster = function (request) {
if (!clusterSync)
return;
clusterSync.getAuthRequests().then(function (doc) {
doc[request.request_id] = request;
return clusterSync.putAuthRequests(doc);
}).then(function () {
return clusterSync.publishAuthDecision(request);
}).catch(function (err) {
server.logger.error("Cluster auth decision publish failed: " + err.message);
});
};
var isLocalRequester = function (request) {
return !clusterSync || !request.node_id || request.node_id === clusterSync.nodeId;
};
var syncLocalAuthRequest = function (request) {
var local = authorizationRequestMap[request.request_id];
if (local && local !== request) {
local.status = request.status;
local.decided_at = request.decided_at;
local.decision_reason = request.decision_reason;
local.role = request.role;
}
};
// ── opt-in HTTP observability surface (Phase 5, revised) ─────────────────
//
// Enabled with `http_api: { enabled: true }` (constructor options or
// settings file) and served on the SAME port as the socket server.
// Read-only: /health, /api/metrics, /api/stats, /api/realms/{realm}/dlq.
// Disabled by default — without the option no HTTP endpoint exists.
var currentIo = null;
var metricsCounters = {};
var incMetric = function (name, labels) {
var labelKeys = labels ? Object.keys(labels).sort() : [];
var key = name + '|' + labelKeys.map(function (k) { return k + '=' + labels[k]; }).join(',');
var entry = metricsCounters[key] = metricsCounters[key] || {name: name, labels: labels || {}, value: 0};
entry.value++;
};
var escapeLabelValue = function (value) {
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
};
var renderPrometheusMetrics = function () {
var lines = [];
var byName = {};
Object.keys(metricsCounters).forEach(function (key) {
var entry = metricsCounters[key];
byName[entry.name] = byName[entry.name] || [];
byName[entry.name].push(entry);
});
Object.keys(byName).sort().forEach(function (name) {
lines.push('# TYPE ' + name + ' counter');
byName[name].forEach(function (entry) {
var labelKeys = Object.keys(entry.labels).sort();
var labelStr = labelKeys.length
? '{' + labelKeys.map(function (k) {
return k + '="' + escapeLabelValue(entry.labels[k]) + '"';
}).join(',') + '}'
: '';
lines.push(name + labelStr + ' ' + entry.value);
});
});
var connected = connectionsCurrent();
if (connected !== null) {
lines.push('# TYPE tyo_mq_connections_current gauge');
lines.push('tyo_mq_connections_current ' + connected);
}
return lines.join('\n') + '\n';
};
var connectionsCurrent = function () {
if (currentIo && currentIo.engine && typeof currentIo.engine.clientsCount === 'number')
return currentIo.engine.clientsCount;
return null;
};
var buildStats = function () {
var stats = {realms: {}, connections_current: connectionsCurrent()};
Object.keys(realms).forEach(function (realmId) {
var realm = realms[realmId];
var producers = {total: 0, online: 0};
var consumers = {total: 0, online: 0};
Object.keys(realm.producers || {}).forEach(function (name) {
producers.total++;
if (realm.producers[name].socket) producers.online++;
});
Object.keys(realm.consumers || {}).forEach(function (name) {
consumers.total++;
if (realm.consumers[name].socket) consumers.online++;
});
stats.realms[realmId] = {
producers: producers,
consumers: consumers,
subscriptions: Object.keys(realm.subscriptions || {}).length
};
});
if (clusterSync)
stats.cluster_node_id = clusterSync.nodeId;
return stats;
};
// Public: snapshot of realm/producer/consumer/subscription counts.
this.getStats = function () {
return buildStats();
};
var getHttpApiConfig = function () {
var config = server.options.http_api || server.options.httpApi
|| server.settings.get('http_api') || server.settings.get('httpApi');
return (config && config.enabled) ? config : null;
};
var httpAuthOk = function (req) {
if (!isAuthEnabled())
return true;
var header = req.headers['authorization'] || '';
var match = header.match(/^Bearer\s+(.+)$/i);
if (!match)
return false;
var provided = hashToken(match[1]);
return getAdminTokens().some(function (token) {
return hashToken(token) === provided;
});
};
var sendJson = function (res, status, body) {
res.writeHead(status, {'content-type': 'application/json; charset=utf-8'});
res.end(JSON.stringify(body));
};
var managementTokens = function () {
var auth = server.authOptions || {};
return (auth.management_tokens || auth.managementTokens || []).filter(function (entry) {
return entry && entry.token && entry.realm_prefix;
});
};
var managementTokenForRequest = function (req) {
var header = req.headers['authorization'] || '';
var match = header.match(/^Bearer\s+(.+)$/i);
if (!match)
return null;
var provided = hashToken(match[1]);
var tokens = managementTokens();
for (var i = 0; i < tokens.length; i++) {
if (hashToken(tokens[i].token) === provided)
return {realm_prefix: tokens[i].realm_prefix};
}
return null;
};
// A management token may only touch realms under its prefix, and never the
// wildcard/default realms or the bare prefix itself.
var realmAllowedForPrefix = function (realm, prefix) {
if (!prefix || typeof realm !== 'string')
return false;
if (realm === REALM_ALL || realm === DEFAULT_REALM)
return false;
return realm.indexOf(prefix) === 0 && realm.length > prefix.length;
};
// Read a bounded JSON request body. Calls back with (err, obj); an empty
// body yields {}. Rejects bodies over maxBytes to avoid unbounded buffering.
var readJsonBody = function (req, maxBytes, callback) {
var chunks = [];
var total = 0;
var done = false;
var finish = function (err, obj) {
if (done) return;
done = true;
callback(err, obj);
};
req.on('data', function (chunk) {
total += chunk.length;
if (total > maxBytes) {
finish(new Error('request body too large'), null);
return;
}
chunks.push(chunk);
});
req.on('end', function () {
var raw = Buffer.concat(chunks).toString('utf8');
if (!raw) { finish(null, {}); return; }
try { finish(null, JSON.parse(raw)); }
catch (err) { finish(err, null); }
});
req.on('error', function (err) { finish(err, null); });
};
// POST /api/realms — create or rotate a realm's manager_key. Returns 404 when
// the endpoint is unconfigured (auth off or no management tokens), 401 without a
// valid management bearer token, 400 on an invalid body, 403 for a realm outside
// the token's prefix, otherwise creates or rotates the realm via
// applyAuthManagementCommand and returns {ok, realm, created, manager_key_configured}.
var handleCreateRealmRequest = function (req, res) {
if (!isAuthEnabled() || managementTokens().length === 0) {
sendJson(res, 404, {ok: false, code: 404, message: 'not found'});
return;
}
var entry = managementTokenForRequest(req);
if (!entry) {
sendJson(res, 401, {ok: false, code: 401, message: 'management token required'});
return;
}
readJsonBody(req, 64 * 1024, function (err, body) {
if (err) {
sendJson(res, 400, {ok: false, code: 400, message: 'invalid JSON body'});
return;
}
body = body || {};
var realm = body.realm;
var managerKey = body.manager_key || body.managerKey;
if (typeof realm !== 'string' || !realm) {
sendJson(res, 400, {ok: false, code: 400, message: 'realm is required'});
return;
}
if (typeof managerKey !== 'string' || !managerKey) {
sendJson(res, 400, {ok: false, code: 400, message: 'manager_key is required'});
return;
}
if (!realmAllowedForPrefix(realm, entry.realm_prefix)) {
sendJson(res, 403, {ok: false, code: 403, message: 'realm outside managed prefix'});
return;
}
var existed = !!(server.authOptions.realms && server.authOptions.realms[realm]);
var result = existed
? applyAuthManagementCommand({command: 'set_realm_manager_key', realm: realm, manager_key: managerKey})
: applyAuthManagementCommand({command: 'add_realm', realm: realm, required: true, manager_key: managerKey});
if (!result || !result.ok) {
var code = (result && result.code) || 500;
sendJson(res, code, {ok: false, code: code, message: (result && result.message) || 'realm update failed'});
return;
}
sendJson(res, 200, {ok: true, realm: realm, created: !existed, manager_key_configured: true});
});
};
var handleHttpApiRequest = function (req, res) {
var config = getHttpApiConfig();
if (!config)
return false;
var pathname;
try {
pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
}
catch (err) {
return false;
}
if (req.method === 'POST' && pathname === '/api/realms') {
handleCreateRealmRequest(req, res);
return true;
}
if (req.method !== 'GET')
return false; // the rest of the surface is read-only by design
if ((pathname === '/health' || pathname === '/healthz') && config.health !== false) {
sendJson(res, 200, {
status: 'ok',
version: pak.version,
uptime_seconds: Math.floor(process.uptime()),
node_id: clusterSync ? clusterSync.nodeId : null
});
return true;
}
if (pathname === '/api/metrics' && config.metrics !== false) {
if (config.metrics_auth !== false && !httpAuthOk(req)) {
sendJson(res, 401, {ok: false, code: 401, message: 'Bearer admin token required'});
return true;
}
res.writeHead(200, {'content-type': 'text/plain; version=0.0.4; charset=utf-8'});
res.end(renderPrometheusMetrics());
return true;
}
if (pathname === '/api/stats' && config.stats !== false) {
if (!httpAuthOk(req)) {
sendJson(res, 401, {ok: false, code: 401, message: 'Bearer admin token required'});
return true;
}
sendJson(res, 200, buildStats());
return true;
}
var dlqMatch = pathname.match(/^\/api\/realms\/([^\/]+)\/dlq$/);
if (dlqMatch && config.stats !== false) {
if (!httpAuthOk(req)) {
sendJson(res, 401, {ok: false, code: 401, message: 'Bearer admin token required'});
return true;
}
if (!server.store || typeof server.store.listDlq !== 'function') {
sendJson(res, 501, {ok: false, code: 501, message: 'Storage backend has no DLQ support'});
return true;
}
server.store.listDlq(dlqMatch[1]).then(function (entries) {
sendJson(res, 200, {realm: dlqMatch[1], entries: entries || []});
}).catch(function (err) {
sendJson(res, 500, {ok: false, code: 500, message: err.message});
});
return true;
}
return false;
};
var applyAuthManagementCommand = function (body) {
body = body || {};
var command = body.command;
var auth = clone(server.authOptions || {});
auth.realms = auth.realms || {};
auth.tokens = auth.tokens || [];
if (command === 'get') {
return {ok: true, settings: publicManagementSettings()};
}
if (command === 'reload_settings') {
if (!loadedSettingsFile)
return {ok: false, code: 400, message: 'No settings file is being watched'};
try {
server.settings.reloadSync();
}
catch (err) {
return {ok: false, code: 500, message: 'Settings reload failed: ' + err.message};
}
return {ok: true, settings: publicManagementSettings()};
}
if (command === 'set_global_auth') {
auth.enabled = !!body.enabled;
}
else if (command === 'set_persistence') {
var normalized = normalizePersistenceSettings(body);
if (!normalized.ok)
return normalized;
try {
applyPersistenceSettings(normalized.settings);
}
catch (err) {
return {ok: false, code: 400, message: err.message};
}
var nextPersistenceSettings = clone(server.settings.get());
nextPersistenceSettings.storage = normalized.settings.storage;
nextPersistenceSettings.storage_options = normalized.settings.storage_options;
applyingPersistenceChange = true;
try {
server.settings.replace(nextPersistenceSettings);
}
finally {
applyingPersistenceChange = false;
}
persistSettings();
return {ok: true, settings: publicManagementSettings()};
}
else if (command === 'add_realm') {
if (!body.realm)
return {ok: false, code: 400, message: 'realm is required'};
if (auth.realms[body.realm])
return {ok: false, code: 409, message: 'realm already exists'};
auth.realms[body.realm] = {required: body.required !== false};
if (body.manager_key || body.managerKey)
auth.realms[body.realm].manager_key = body.manager_key || body.managerKey;
if (body.key || body.preshared_key || body.presharedKey)
auth.realms[body.realm].key = body.key || body.preshared_key || body.presharedKey;
if (body.require_acceptance !== undefined && body.require_acceptance !== null)
auth.realms[body.realm].require_acceptance = !!body.require_acceptance;
}
else if (command === 'rename_realm') {
if (!body.from || !body.to)
return {ok: false, code: 400, message: 'from and to are required'};
if (!auth.realms[body.from])
return {ok: false, code: 404, message: 'realm not found'};
if (auth.realms[body.to])
return {ok: false, code: 409, message: 'target realm already exists'};
auth.realms[body.to] = auth.realms[body.from];
delete auth.realms[body.from];
auth.tokens.forEach(function (token) {
if (token.realm === body.from)
token.realm = body.to;
});
if (realms[body.from] && !realms[body.to]) {
realms[body.to] = realms[body.from];
delete realms[body.from];
}
}
else if (command === 'remove_realm') {
if (!body.realm)
return {ok: false, code: 400, message: 'realm is required'};
if (body.realm === DEFAULT_REALM || body.realm === REALM_ALL)
return {ok: false, code: 400, message: 'cannot remove the "' + body.realm + '" realm'};
if (!auth.realms[body.realm])
return {ok: false, code: 404, message: 'realm not found'};
delete auth.realms[body.realm];
// Drop tokens scoped to the removed realm — they would be orphaned.
var tokensBefore = auth.tokens.length;
auth.tokens = auth.tokens.filter(function (token) {
return token.realm !== body.realm;
});
body._removed_tokens = tokensBefore - auth.tokens.length;
// Drop in-memory runtime state (producers/consumers/subscriptions).
if (realms[body.realm])
delete realms[body.realm];
}
else if (command === 'set_realm_auth') {
if (!body.realm)
return {ok: false, code: 400, message: 'realm is required'};
auth.realms[body.realm] = auth.realms[body.realm] || {};
auth.realms[body.realm].required = !!body.required;
}
else if (command === 'set_realm_manager_key') {
if (!body.realm)
return {ok: false, code: 400, message: 'realm is required'};
if (!auth.realms[body.realm])
return {ok: false, code: 404, message: 'realm not found'};
if (body.manager_key || body.managerKey)
auth.realms[body.realm].manager_key = body.manager_key || body.managerKey;
else
delete auth.realms[body.realm].manager_key;
}
else if (command === 'set_realm_key') {
if (!body.realm)
return {ok: false, code: 400, message: 'realm is required'};
if (!auth.realms[body.realm])
return {ok: false, code: 404, message: 'realm not found'};
if (body.key || body.preshared_key || body.presharedKey)
auth.realms[body.realm].key = body.key || body.preshared_key || body.presharedKey;
else
delete auth.realms[body.realm].key;
if (body.require_key !== undefined && body.require_key !== null)
auth.realms[body.realm].require_key = !!body.require_key;
}
else if (command === 'set_realm_acceptance') {
if (!body.realm)
return {ok: false, code: 400, message: 'realm is required'};
auth.realms[body.realm] = auth.realms[body.realm] || {};
auth.realms[body.realm].require_acceptance = !!body.required;
}
else if (command === 'add_management_token') {
if (!body.token || typeof body.token !== 'string')
return {ok: false, code: 400, message: 'token is required'};
if (!body.realm_prefix || typeof body.realm_prefix !== 'string')
return {ok: false, code: 400, message: 'realm_prefix is required'};
auth.management_tokens = auth.management_tokens || auth.managementTokens || [];
delete auth.managementTokens;
var mgmtExists = auth.management_tokens.some(function (entry) {
return entry && entry.token === body.token;
});
if (mgmtExists)
return {ok: false, code: 409, message: 'management token already exists'};
auth.management_tokens.push({
token: body.token,
realm_prefix: body.realm_prefix
});
}
else if (command === 'revoke_management_token') {
if (!body.token_hash)
return {ok: false, code: 400, message: 'token_hash is required'};
var mgmtTokens = auth.management_tokens || auth.managementTokens || [];
var mgmtBefore = mgmtTokens.length;
mgmtTokens = mgmtTokens.filter(function (entry) {
return !(entry && entry.token && hashToken(entry.token) === body.token_hash);
});
if (mgmtTokens.length === mgmtBefore)
return {ok: false, code: 404, message: 'management token not found'};
auth.management_tokens = mgmtTokens;
delete auth.managementTokens;
}
else if (command === 'set_external_auth') {
if (body.auth_url) {
try {
new URL(String(body.auth_url));
}
catch (err) {
return {ok: false, code: 400, message: 'auth_url must be a valid URL'};
}
auth.auth_url = String(body.auth_url);
}
else {
delete auth.auth_url;
}
// Omitted secret keeps the current one (managers can't read it back);
// an explicit empty string clears it.
if (body.auth_secret !== undefined && body.auth_secret !== null) {
if (body.auth_secret)
auth.auth_secret = String(body.auth_secret);
else
delete auth.auth_secret;
}
}
else if (command === 'revoke_token') {
var revoked = revokeAuthTokens(auth, body);
if (!revoked.ok)
return revoked;
body._revoked = revoked.revoked;
}
else {
return {ok: false, code: 400, message: 'unknown management command'};
}
var nextSettings = clone(server.settings.get());
nextSettings.auth = auth;
server.settings.replace(nextSettings);
persistSettings();
var result = {ok: true, settings: publicManagementSettings()};
if (body._revoked)
result.revoked = body._revoked;
if (body._removed_tokens !== undefined)
result.removed_tokens = body._removed_tokens;
return result;
};
var nextPendingAuthorizationRequest = function (filter) {
filter = filter || {};
for (var i = 0; i < authorizationRequests.length; i++) {
var request = authorizationRequests[i];
if (request.status !== 'pending')
continue;
if (filter.realm && request.realm !== filter.realm)
continue;
return request;
}
return null;
};
var authorizationClientKey = function (realm, clientId) {
return String(realm) + '\n' + String(clientId);
};
var saveLatestAuthorizationRequest = function (request) {
var key = authorizationClientKey(request.realm, request.client_id);
var previous = authorizationRequestClientMap[key];
if (previous && previous.status === 'pending') {
previous.status = 'superseded';
previous.superseded_at = request.created_at;
delete authorizationRequestMap[previous.request_id];
}
authorizationRequestClientMap[key] = request;
authorizationRequests.push(request);
authorizationRequestMap[request.request_id] = request;
};
var getRealm = function (realmId) {
realmId = realmId || DEFAULT_REALM;
realms[realmId] = realms[realmId] || {producers: {}, consumers: {}, subscriptions: {}};
return realms[realmId];
};
var getSocketRealmId = function (socket) {
return (socket.tyoAuth && socket.tyoAuth.realm) || DEFAULT_REALM;
};
var getSocketRealm = function (socket) {
return getRealm(getSocketRealmId(socket));
};
// Global auth toggle — true if auth.enabled is set in current settings.
var isAuthEnabled = function () {
return !!(server.settings.get('auth') && server.settings.get('auth').enabled);
};
// Per-realm auth requirement. Resolution order:
// 1. auth.realms[realmId].required (explicit per-realm override)
// 2. auth.enabled (global default)
// Returns true when auth is required for this realm.
var isAuthRequiredForRealm = function (realmId) {
if (!isAuthEnabled()) return false;
var auth = server.settings.get('auth') || {};
if (realmId && auth.realms && auth.realms[realmId]) {
var rc = auth.realms[realmId];
if (typeof rc.required === 'boolean') return rc.required;
}
return true;
};
var normalizeRole = function (role) {
return role || 'both';
};
// Roles a client may declare for itself on connection. 'manager' and
// 'admin' are deliberately excluded — those are only granted via tokens.
var CONNECT_ROLES = ['producer', 'consumer', 'both'];
// MQTT-style topic matching: '+' matches exactly one level, '#' matches
// any number of trailing levels (must be the last segment).
var topicMatches = function (pattern, topic) {
if (pattern =