From 5d9628ce7026b429fe225f935ad9344828efe4b3 Mon Sep 17 00:00:00 2001 From: coderiaser Date: Tue, 26 May 2026 00:17:44 +0300 Subject: [PATCH] feature: cloudcmd: rate limit: add support (#437) --- bin/cloudcmd.js | 2 +- package.json | 1 + server/cloudcmd.spec.js | 17 ----------------- server/server.js | 16 ++++++++++++++-- test/e2e/ratelimit.js | 22 ++++++++++++++++++++++ 5 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 test/e2e/ratelimit.js diff --git a/bin/cloudcmd.js b/bin/cloudcmd.js index bdf88379..6f057a1f 100755 --- a/bin/cloudcmd.js +++ b/bin/cloudcmd.js @@ -115,7 +115,7 @@ const yargsOptions = { 'columns': env.parse('columns') || config('columns') || '', 'theme': env.parse('theme') || config('theme') || '', 'vim': choose(env.bool('vim'), config('vim')), - 'log': config('log'), + 'log': choose(env.bool('log'), config('log')), 'import-url': env.parse('import_url') || config('importUrl'), 'import-listen': choose(env.bool('import_listen'), config('importListen')), diff --git a/package.json b/package.json index a1838284..3193d6f0 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "es6-promisify": "^7.0.0", "execon": "^1.2.0", "express": "^5.1.0", + "express-rate-limit": "^8.5.2", "files-io": "^4.0.0", "find-up": "^8.0.0", "for-each-key": "^2.0.0", diff --git a/server/cloudcmd.spec.js b/server/cloudcmd.spec.js index ace68e25..97949cd3 100644 --- a/server/cloudcmd.spec.js +++ b/server/cloudcmd.spec.js @@ -178,20 +178,3 @@ test('cloudcmd: sw', async (t) => { t.equal(status, 200, 'should return sw'); t.end(); }); - -test('cloudcmd: manifest.json', async (t) => { - const config = { - auth: true, - }; - - const options = { - config, - }; - - const {status} = await request.get('/public/manifest.json', { - options, - }); - - t.equal(status, 200, 'should return manifest.json even when authentication is enabled'); - t.end(); -}); diff --git a/server/server.js b/server/server.js index e9ad5145..f1d0d405 100644 --- a/server/server.js +++ b/server/server.js @@ -1,18 +1,22 @@ import http from 'node:http'; import {promisify} from 'node:util'; import process from 'node:process'; +import {rateLimit} from 'express-rate-limit'; import currify from 'currify'; import squad from 'squad'; import {tryToCatch} from 'try-to-catch'; import opn from 'open'; import express from 'express'; import {Server} from 'socket.io'; -import tryRequire from 'tryrequire'; import wraptile from 'wraptile'; import compression from 'compression'; +import tryRequire from 'tryrequire'; import {cloudcmd} from '#server/cloudcmd'; import exit from './exit.js'; +const RATE_LIMIT = 1000; +const RATE_WINDOW = 15 * 60 * 1000; + const bind = (f, self) => f.bind(self); const two = currify((f, a, b) => f(a, b)); @@ -25,6 +29,7 @@ const shutdown = wraptile(async (promises) => { const promisifySelf = squad(promisify, bind); const exitPort = two(exit, 'cloudcmd --port: %s'); + const logger = tryRequire('morgan'); export default async (options, config) => { @@ -46,8 +51,13 @@ export default async (options, config) => { path: `${prefix}/socket.io`, }); - app.use(compression()); + const limiter = rateLimit({ + windowMs: RATE_WINDOW, + limit: RATE_LIMIT, + }); + app.use(compression()); + app.use(limiter); app.use(prefix, cloudcmd({ config: options, socket: socketServer, @@ -65,7 +75,9 @@ export default async (options, config) => { await listen(port, ip); const close = shutdown([closeServer, closeSocket]); + process.on('SIGINT', close); + process.on('SIUSR1', close); const host = config('ip') || 'localhost'; const port0 = port || server.address().port; diff --git a/test/e2e/ratelimit.js b/test/e2e/ratelimit.js new file mode 100644 index 00000000..75811aa1 --- /dev/null +++ b/test/e2e/ratelimit.js @@ -0,0 +1,22 @@ +import process from 'node:process'; +import {test} from 'supertape'; + +test('cloudcmd: server: ratelimit', async (t) => { + const PORT = 3000; + const STATUS = 429; + + process.env.PORT = PORT; + process.env.CLOUDCMD_LOG = 0; + + await import('../../bin/cloudcmd.js'); + + for (let i = 0; i < 1000; i++) { + await fetch(`http://localhost:${PORT}`); + } + + const {status} = await fetch(`http://localhost:${PORT}`); + process.kill(process.pid, 'SIGUSR1'); + + t.equal(status, STATUS); + t.end(); +});