From 06ee65b55debd14835c05315994ca3f517256f2d Mon Sep 17 00:00:00 2001 From: jj Date: Sat, 24 May 2025 14:32:50 +0000 Subject: [PATCH] api/api-keys: watch for file changes instead of polling --- api/src/misc/file-watcher.js | 43 ++++++++++++++++++++++++++++++ api/src/security/api-keys.js | 51 ++++++++++++++++++++---------------- 2 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 api/src/misc/file-watcher.js diff --git a/api/src/misc/file-watcher.js b/api/src/misc/file-watcher.js new file mode 100644 index 00000000..d66a77be --- /dev/null +++ b/api/src/misc/file-watcher.js @@ -0,0 +1,43 @@ +import { EventEmitter } from 'node:events'; +import * as fs from 'node:fs/promises'; + +export class FileWatcher extends EventEmitter { + #path; + #hasWatcher = false; + #lastChange = new Date().getTime(); + + constructor({ path, ...rest }) { + super(rest); + this.#path = path; + } + + async #setupWatcher() { + if (this.#hasWatcher) + return; + + this.#hasWatcher = true; + const watcher = fs.watch(this.#path); + for await (const _ of watcher) { + if (new Date() - this.#lastChange > 50) { + this.emit('file-updated'); + this.#lastChange = new Date().getTime(); + } + } + } + + read() { + this.#setupWatcher(); + return fs.readFile(this.#path, 'utf8'); + } + + static fromFileProtocol(url_) { + const url = new URL(url_); + if (url.protocol !== 'file:') { + return; + } + + const pathname = url.pathname === '/' ? '' : url.pathname; + const file_path = decodeURIComponent(url.host + pathname); + return new this({ path: file_path }); + } +} diff --git a/api/src/security/api-keys.js b/api/src/security/api-keys.js index d534999c..37ec66fb 100644 --- a/api/src/security/api-keys.js +++ b/api/src/security/api-keys.js @@ -1,8 +1,8 @@ import { env } from "../config.js"; -import { readFile } from "node:fs/promises"; import { Green, Yellow } from "../misc/console-text.js"; import ip from "ipaddr.js"; import * as cluster from "../misc/cluster.js"; +import { FileWatcher } from "../misc/file-watcher.js"; // this function is a modified variation of code // from https://stackoverflow.com/a/32402438/14855621 @@ -13,7 +13,7 @@ const generateWildcardRegex = rule => { const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; -let keys = {}; +let keys = {}, reader = null; const ALLOWED_KEYS = new Set(['name', 'ips', 'userAgents', 'limit']); @@ -118,34 +118,39 @@ const formatKeys = (keyData) => { } const updateKeys = (newKeys) => { + validateKeys(newKeys); + + cluster.broadcast({ api_keys: newKeys }); + keys = formatKeys(newKeys); } -const loadKeys = async (source) => { - let updated; - if (source.protocol === 'file:') { - const pathname = source.pathname === '/' ? '' : source.pathname; - updated = JSON.parse( - await readFile( - decodeURIComponent(source.host + pathname), - 'utf8' - ) - ); - } else { - updated = await fetch(source).then(a => a.json()); - } +const loadRemoteKeys = async (source) => { + updateKeys( + await fetch(source).then(a => a.json()) + ); +} - validateKeys(updated); - - cluster.broadcast({ api_keys: updated }); - - updateKeys(updated); +const loadLocalKeys = async () => { + updateKeys( + JSON.parse(await reader.read()) + ); } const wrapLoad = (url, initial = false) => { - loadKeys(url) - .then(() => { + let load = loadRemoteKeys.bind(null, url); + + if (url.protocol === 'file:') { if (initial) { + reader = FileWatcher.fromFileProtocol(url); + reader.on('file-updated', () => wrapLoad(url)); + } + + load = loadLocalKeys; + } + + load().then(() => { + if (initial || reader) { console.log(`${Green('[✓]')} api keys loaded successfully!`) } }) @@ -214,7 +219,7 @@ export const validateAuthorization = (req) => { export const setup = (url) => { if (cluster.isPrimary) { wrapLoad(url, true); - if (env.keyReloadInterval > 0) { + if (env.keyReloadInterval > 0 && url.protocol !== 'file:') { setInterval(() => wrapLoad(url), env.keyReloadInterval * 1000); } } else if (cluster.isWorker) {