diff --git a/app.js b/app.js index 9262b47..466682d 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,5 @@ const express = require('express'); +const rateLimit = require('express-rate-limit'); const path = require('path'); const bodyParser = require('body-parser'); const API = require('./src/js/index.js'); @@ -11,7 +12,7 @@ require('./src/js/utils.js'); app.set('trust proxy', true); // Middleware -app.use(bodyParser.json({ limit: '10mb' })); +// app.use(bodyParser.json({ limit: '10mb' })); app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' })); app.use(express.static(__dirname)); app.use(express.static(path.join(__dirname, 'public'))); @@ -29,6 +30,153 @@ app.use( const fs = require('fs'); +let DEFAULT_SSO_TOKEN = ''; + +// Try to read the token from a file +try { + const tokenPath = path.join(__dirname, 'token.txt'); + if (fs.existsSync(tokenPath)) { + DEFAULT_SSO_TOKEN = fs.readFileSync(tokenPath, 'utf8').trim(); + logger.info('Default SSO token loaded successfully from token.txt'); + } else { + logger.warn('token.txt not found, demo mode may not function correctly'); + } +} catch (error) { + logger.error('Error loading token file:', { error: error.message }); +} + +setInterval(() => demoModeRequestTracker.cleanupExpiredEntries(), 3600000); // Clean up every hour + +// Configure rate limiting for demo mode +const demoModeRequestTracker = { + requests: new Map(), + maxRequestsPerHour: 5, // Adjust as needed + resetInterval: 60 * 60 * 1000, // 1 hour in milliseconds + + isLimited: function (ip) { + const now = Date.now(); + const userRequests = this.requests.get(ip) || { + count: 0, + resetTime: now + this.resetInterval, + }; + + // Reset counter if the reset time has passed + if (now > userRequests.resetTime) { + userRequests.count = 0; + userRequests.resetTime = now + this.resetInterval; + } + + return userRequests.count >= this.maxRequestsPerHour; + }, + + incrementCounter: function (ip) { + const now = Date.now(); + const userRequests = this.requests.get(ip) || { + count: 0, + resetTime: now + this.resetInterval, + }; + + // Reset counter if the reset time has passed + if (now > userRequests.resetTime) { + userRequests.count = 1; // Start with 1 for this request + userRequests.resetTime = now + this.resetInterval; + } else { + userRequests.count++; + } + + this.requests.set(ip, userRequests); + return userRequests.count; + }, + + cleanupExpiredEntries: function () { + const now = Date.now(); + for (const [ip, data] of this.requests.entries()) { + if (now > data.resetTime) { + // Either remove entries completely or reset them to 0 + this.requests.delete(ip); + } + } + }, +}; + +// Demo mode middleware +const demoModeMiddleware = (req, res, next) => { + // Skip non-API routes + if (!req.path.startsWith('/api/')) { + return next(); + } + + // Get client IP for tracking + // const clientIP = req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress; + const clientIP = + req.headers['cf-connecting-ip'] || + req.headers['x-forwarded-for']?.split(',')[0] || + req.ip || + req.connection.remoteAddress; + + // Check if demo mode is active + if (DEFAULT_SSO_TOKEN) { + // For API endpoints, check rate limit in demo mode + if (demoModeRequestTracker.isLimited(clientIP)) { + const userRequests = demoModeRequestTracker.requests.get(clientIP); + const resetTimeMs = userRequests.resetTime - Date.now(); + + // Format time as HH:MM:SS + const hours = Math.floor(resetTimeMs / (60 * 60 * 1000)); + const minutes = Math.floor( + (resetTimeMs % (60 * 60 * 1000)) / (60 * 1000) + ); + const seconds = Math.floor((resetTimeMs % (60 * 1000)) / 1000); + const timeFormat = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + logger.warn(`Demo mode rate limit exceeded for IP: ${clientIP}`); + return res.status(429).json({ + status: 'error', + message: `Please try again in ${timeFormat} or use your own SSO token.`, + timestamp: global.Utils.toIsoString(new Date()), + }); + } + + // Increment the counter + // const count = demoModeRequestTracker.incrementCounter(clientIP); + // logger.debug(`Demo mode request count for ${clientIP}: ${count}`); + } + + next(); +}; + +function incrementDemoCounter(req, ssoToken) { + if (ssoToken === DEFAULT_SSO_TOKEN) { + const clientIP = + req.headers['cf-connecting-ip'] || + req.headers['x-forwarded-for']?.split(',')[0] || + req.ip || + req.connection.remoteAddress; + + const count = demoModeRequestTracker.incrementCounter(clientIP); + logger.debug(`Demo mode request count for ${clientIP}: ${count}`); + } +} + +app.use(demoModeMiddleware); +app.use((req, res, next) => { + if (req.query.demo === 'true' || req.query.demo === '1') { + // Enable demo mode for this session if it's not already enabled + if (!req.session) { + req.session = {}; + } + req.session.forceDemoMode = true; + logger.info('Demo mode enabled via URL parameter'); + } else if (req.query.demo === 'false' || req.query.demo === '0') { + // Disable demo mode for this session + if (req.session) { + req.session.forceDemoMode = false; + } + logger.info('Demo mode disabled via URL parameter'); + } + next(); +}); + // Initialize key replacements let keyReplacements = {}; @@ -224,15 +372,19 @@ app.post('/api/stats', async (req, res) => { ); try { - const { - username, - ssoToken, - platform, - game, - apiCall, - sanitize, - replaceKeys, - } = req.body; + let { username, ssoToken, platform, game, apiCall, sanitize, replaceKeys } = + req.body; + + if (!ssoToken && DEFAULT_SSO_TOKEN) { + ssoToken = DEFAULT_SSO_TOKEN; + logger.info('Using default SSO token for demo mode'); + } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + return res.status(200).json({ + status: 'error', + message: 'SSO Token is required as demo mode is not active', + timestamp: global.Utils.toIsoString(new Date()), + }); + } /* logger.debug( @@ -247,10 +399,6 @@ app.post('/api/stats', async (req, res) => { logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("====================="); */ - if (!ssoToken) { - return res.status(400).json({ error: 'SSO Token is required' }); - } - // For mapList, username is not required if (apiCall !== 'mapList' && !username) { return res.status(400).json({ error: 'Username is required' }); @@ -435,6 +583,8 @@ app.post('/api/stats', async (req, res) => { const { sanitize, replaceKeys } = req.body; + incrementDemoCounter(req, ssoToken); + return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), @@ -475,7 +625,7 @@ app.post('/api/matches', async (req, res) => { ); try { - const { username, ssoToken, platform, game, sanitize, replaceKeys } = + let { username, ssoToken, platform, game, sanitize, replaceKeys } = req.body; /* @@ -490,10 +640,19 @@ app.post('/api/matches', async (req, res) => { logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("========================"); */ - if (!username || !ssoToken) { - return res - .status(400) - .json({ error: 'Username and SSO Token are required' }); + if (!username) { + return res.status(400).json({ error: 'Username is required' }); + } + + if (!ssoToken && DEFAULT_SSO_TOKEN) { + ssoToken = DEFAULT_SSO_TOKEN; + logger.info('Using default SSO token for demo mode'); + } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + return res.status(200).json({ + status: 'error', + message: 'SSO Token is required as demo mode is not active', + timestamp: global.Utils.toIsoString(new Date()), + }); } try { @@ -584,6 +743,8 @@ app.post('/api/matches', async (req, res) => { const { sanitize, replaceKeys } = req.body; + incrementDemoCounter(req, ssoToken); + return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), @@ -621,9 +782,7 @@ app.post('/api/matchInfo', async (req, res) => { ); try { - const { matchId, ssoToken, platform, game, sanitize, replaceKeys } = - req.body; - const mode = 'mp'; + let { matchId, ssoToken, platform, game, sanitize, replaceKeys } = req.body; /* logger.debug( @@ -637,10 +796,19 @@ app.post('/api/matchInfo', async (req, res) => { logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("=========================="); */ - if (!matchId || !ssoToken) { - return res - .status(400) - .json({ error: 'Match ID and SSO Token are required' }); + if (!matchId) { + return res.status(400).json({ error: 'Match ID is required' }); + } + + if (!ssoToken && DEFAULT_SSO_TOKEN) { + ssoToken = DEFAULT_SSO_TOKEN; + logger.info('Using default SSO token for demo mode'); + } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + return res.status(200).json({ + status: 'error', + message: 'SSO Token is required as demo mode is not active', + timestamp: global.Utils.toIsoString(new Date()), + }); } try { @@ -717,6 +885,8 @@ app.post('/api/matchInfo', async (req, res) => { const { sanitize, replaceKeys } = req.body; + incrementDemoCounter(req, ssoToken); + return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), @@ -754,7 +924,7 @@ app.post('/api/user', async (req, res) => { ); try { - const { username, ssoToken, platform, userCall, sanitize, replaceKeys } = + let { username, ssoToken, platform, userCall, sanitize, replaceKeys } = req.body; /* @@ -769,10 +939,6 @@ app.post('/api/user', async (req, res) => { logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("========================="); */ - if (!ssoToken) { - return res.status(400).json({ error: 'SSO Token is required' }); - } - // For eventFeed and identities, username is not required if ( !username && @@ -785,6 +951,17 @@ app.post('/api/user', async (req, res) => { .json({ error: 'Username is required for this API call' }); } + if (!ssoToken && DEFAULT_SSO_TOKEN) { + ssoToken = DEFAULT_SSO_TOKEN; + logger.info('Using default SSO token for demo mode'); + } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + return res.status(200).json({ + status: 'error', + message: 'SSO Token is required as demo mode is not active', + timestamp: global.Utils.toIsoString(new Date()), + }); + } + try { await ensureLogin(ssoToken); } catch (loginError) { @@ -850,6 +1027,8 @@ app.post('/api/user', async (req, res) => { const { sanitize, replaceKeys } = req.body; + incrementDemoCounter(req, ssoToken); + return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), @@ -886,23 +1065,33 @@ app.post('/api/search', async (req, res) => { ); try { - const { username, ssoToken, platform, sanitize, replaceKeys } = req.body; + let { username, ssoToken, platform, sanitize, replaceKeys } = req.body; /* logger.debug( - `Request details - Username to search: ${username}, Platform: ${platform}` + `Request details - Match ID: ${matchId}, Platform: ${platform}, Game: ${game}` ); - logger.debug("=== SEARCH REQUEST ==="); - logger.debug(`Search Term: ${username}`); + logger.debug("=== MATCH INFO REQUEST ==="); + logger.debug(`Match ID: ${matchId}`); logger.debug(`Platform: ${platform}`); + logger.debug(`Game: ${game}`); logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); - logger.debug("======================"); */ + logger.debug("=========================="); */ - if (!username || !ssoToken) { - return res - .status(400) - .json({ error: 'Username and SSO Token are required' }); + if (!username) { + return res.status(400).json({ error: 'Username is required' }); + } + + if (!ssoToken && DEFAULT_SSO_TOKEN) { + ssoToken = DEFAULT_SSO_TOKEN; + logger.info('Using default SSO token for demo mode'); + } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + return res.status(200).json({ + status: 'error', + message: 'SSO Token is required as demo mode is not active', + timestamp: global.Utils.toIsoString(new Date()), + }); } try { @@ -935,6 +1124,8 @@ app.post('/api/search', async (req, res) => { const { sanitize, replaceKeys } = req.body; + incrementDemoCounter(req, ssoToken); + return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), @@ -957,7 +1148,10 @@ app.post('/api/search', async (req, res) => { // Improved logging endpoint app.post('/api/log', (req, res) => { const clientIP = - req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress; + req.headers['cf-connecting-ip'] || + req.headers['x-forwarded-for']?.split(',')[0] || + req.ip || + req.connection.remoteAddress; const userAgent = req.headers['user-agent']; const referer = req.headers['referer']; const origin = req.headers['origin']; @@ -1028,9 +1222,32 @@ function storeLogInDatabase(logData) { } */ -// Basic health check endpoint app.get('/health', (req, res) => { - res.json({ status: 'ok', timestamp: global.Utils.toIsoString(new Date()) }); + // Check if demo mode is forced via URL parameter + const forceDemoMode = req.session && req.session.forceDemoMode === true; + + const isDemoMode = !!DEFAULT_SSO_TOKEN || forceDemoMode; + res.json({ + status: 'ok', + timestamp: global.Utils.toIsoString(new Date()), + demoMode: isDemoMode, + requestsRemaining: + isDemoMode ? + demoModeRequestTracker.maxRequestsPerHour - + (demoModeRequestTracker.requests.get(req.ip)?.count || 0) + : null, + }); +}); + +app.get('/demo', (req, res) => { + // Set a cookie or session variable to enable demo mode + if (!req.session) { + req.session = {}; + } + req.session.forceDemoMode = true; + + // Redirect to the main app + res.redirect('/'); }); // Serve the main HTML file diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index e9c8eb3..db58e60 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -62,6 +62,15 @@ "node": ">=6.9.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -508,6 +517,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-of-duty-api": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/call-of-duty-api/-/call-of-duty-api-4.1.0.tgz", + "integrity": "sha512-f5DJ6gQru6f406QVBZkkXOv0gUzFu0hykdkyKRa2Am6iWwGRVzcBK7rq+xHpNI6oTq3EFfw0T70kb38rZaezNA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0", + "undici": "^5.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Lierrmm" + } + }, + "node_modules/call-of-duty-api/node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/camel-case": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", @@ -987,6 +1023,55 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/express/node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1869,6 +1954,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2045,6 +2139,21 @@ "node": ">=10" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2116,6 +2225,15 @@ ], "license": "MIT" }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2849,6 +2967,18 @@ "node": ">=0.8.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undici": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz", diff --git a/node_modules/@fastify/busboy/deps/dicer/lib/Dicer.js b/node_modules/@fastify/busboy/deps/dicer/lib/Dicer.js new file mode 100644 index 0000000..3c8c081 --- /dev/null +++ b/node_modules/@fastify/busboy/deps/dicer/lib/Dicer.js @@ -0,0 +1,213 @@ +'use strict' + +const WritableStream = require('node:stream').Writable +const inherits = require('node:util').inherits + +const StreamSearch = require('../../streamsearch/sbmh') + +const PartStream = require('./PartStream') +const HeaderParser = require('./HeaderParser') + +const DASH = 45 +const B_ONEDASH = Buffer.from('-') +const B_CRLF = Buffer.from('\r\n') +const EMPTY_FN = function () {} + +function Dicer (cfg) { + if (!(this instanceof Dicer)) { return new Dicer(cfg) } + WritableStream.call(this, cfg) + + if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') } + + if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined } + + this._headerFirst = cfg.headerFirst + + this._dashes = 0 + this._parts = 0 + this._finished = false + this._realFinish = false + this._isPreamble = true + this._justMatched = false + this._firstWrite = true + this._inHeader = true + this._part = undefined + this._cb = undefined + this._ignoreData = false + this._partOpts = { highWaterMark: cfg.partHwm } + this._pause = false + + const self = this + this._hparser = new HeaderParser(cfg) + this._hparser.on('header', function (header) { + self._inHeader = false + self._part.emit('header', header) + }) +} +inherits(Dicer, WritableStream) + +Dicer.prototype.emit = function (ev) { + if (ev === 'finish' && !this._realFinish) { + if (!this._finished) { + const self = this + process.nextTick(function () { + self.emit('error', new Error('Unexpected end of multipart data')) + if (self._part && !self._ignoreData) { + const type = (self._isPreamble ? 'Preamble' : 'Part') + self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data')) + self._part.push(null) + process.nextTick(function () { + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + return + } + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + } + } else { WritableStream.prototype.emit.apply(this, arguments) } +} + +Dicer.prototype._write = function (data, encoding, cb) { + // ignore unexpected data (e.g. extra trailer data after finished) + if (!this._hparser && !this._bparser) { return cb() } + + if (this._headerFirst && this._isPreamble) { + if (!this._part) { + this._part = new PartStream(this._partOpts) + if (this.listenerCount('preamble') !== 0) { this.emit('preamble', this._part) } else { this._ignore() } + } + const r = this._hparser.push(data) + if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() } + } + + // allows for "easier" testing + if (this._firstWrite) { + this._bparser.push(B_CRLF) + this._firstWrite = false + } + + this._bparser.push(data) + + if (this._pause) { this._cb = cb } else { cb() } +} + +Dicer.prototype.reset = function () { + this._part = undefined + this._bparser = undefined + this._hparser = undefined +} + +Dicer.prototype.setBoundary = function (boundary) { + const self = this + this._bparser = new StreamSearch('\r\n--' + boundary) + this._bparser.on('info', function (isMatch, data, start, end) { + self._oninfo(isMatch, data, start, end) + }) +} + +Dicer.prototype._ignore = function () { + if (this._part && !this._ignoreData) { + this._ignoreData = true + this._part.on('error', EMPTY_FN) + // we must perform some kind of read on the stream even though we are + // ignoring the data, otherwise node's Readable stream will not emit 'end' + // after pushing null to the stream + this._part.resume() + } +} + +Dicer.prototype._oninfo = function (isMatch, data, start, end) { + let buf; const self = this; let i = 0; let r; let shouldWriteMore = true + + if (!this._part && this._justMatched && data) { + while (this._dashes < 2 && (start + i) < end) { + if (data[start + i] === DASH) { + ++i + ++this._dashes + } else { + if (this._dashes) { buf = B_ONEDASH } + this._dashes = 0 + break + } + } + if (this._dashes === 2) { + if ((start + i) < end && this.listenerCount('trailer') !== 0) { this.emit('trailer', data.slice(start + i, end)) } + this.reset() + this._finished = true + // no more parts will be added + if (self._parts === 0) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } + } + if (this._dashes) { return } + } + if (this._justMatched) { this._justMatched = false } + if (!this._part) { + this._part = new PartStream(this._partOpts) + this._part._read = function (n) { + self._unpause() + } + if (this._isPreamble && this.listenerCount('preamble') !== 0) { + this.emit('preamble', this._part) + } else if (this._isPreamble !== true && this.listenerCount('part') !== 0) { + this.emit('part', this._part) + } else { + this._ignore() + } + if (!this._isPreamble) { this._inHeader = true } + } + if (data && start < end && !this._ignoreData) { + if (this._isPreamble || !this._inHeader) { + if (buf) { shouldWriteMore = this._part.push(buf) } + shouldWriteMore = this._part.push(data.slice(start, end)) + if (!shouldWriteMore) { this._pause = true } + } else if (!this._isPreamble && this._inHeader) { + if (buf) { this._hparser.push(buf) } + r = this._hparser.push(data.slice(start, end)) + if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) } + } + } + if (isMatch) { + this._hparser.reset() + if (this._isPreamble) { this._isPreamble = false } else { + if (start !== end) { + ++this._parts + this._part.on('end', function () { + if (--self._parts === 0) { + if (self._finished) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } else { + self._unpause() + } + } + }) + } + } + this._part.push(null) + this._part = undefined + this._ignoreData = false + this._justMatched = true + this._dashes = 0 + } +} + +Dicer.prototype._unpause = function () { + if (!this._pause) { return } + + this._pause = false + if (this._cb) { + const cb = this._cb + this._cb = undefined + cb() + } +} + +module.exports = Dicer diff --git a/node_modules/@fastify/busboy/deps/dicer/lib/HeaderParser.js b/node_modules/@fastify/busboy/deps/dicer/lib/HeaderParser.js new file mode 100644 index 0000000..65f667b --- /dev/null +++ b/node_modules/@fastify/busboy/deps/dicer/lib/HeaderParser.js @@ -0,0 +1,100 @@ +'use strict' + +const EventEmitter = require('node:events').EventEmitter +const inherits = require('node:util').inherits +const getLimit = require('../../../lib/utils/getLimit') + +const StreamSearch = require('../../streamsearch/sbmh') + +const B_DCRLF = Buffer.from('\r\n\r\n') +const RE_CRLF = /\r\n/g +const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex + +function HeaderParser (cfg) { + EventEmitter.call(this) + + cfg = cfg || {} + const self = this + this.nread = 0 + this.maxed = false + this.npairs = 0 + this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000) + this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024) + this.buffer = '' + this.header = {} + this.finished = false + this.ss = new StreamSearch(B_DCRLF) + this.ss.on('info', function (isMatch, data, start, end) { + if (data && !self.maxed) { + if (self.nread + end - start >= self.maxHeaderSize) { + end = self.maxHeaderSize - self.nread + start + self.nread = self.maxHeaderSize + self.maxed = true + } else { self.nread += (end - start) } + + self.buffer += data.toString('binary', start, end) + } + if (isMatch) { self._finish() } + }) +} +inherits(HeaderParser, EventEmitter) + +HeaderParser.prototype.push = function (data) { + const r = this.ss.push(data) + if (this.finished) { return r } +} + +HeaderParser.prototype.reset = function () { + this.finished = false + this.buffer = '' + this.header = {} + this.ss.reset() +} + +HeaderParser.prototype._finish = function () { + if (this.buffer) { this._parseHeader() } + this.ss.matches = this.ss.maxMatches + const header = this.header + this.header = {} + this.buffer = '' + this.finished = true + this.nread = this.npairs = 0 + this.maxed = false + this.emit('header', header) +} + +HeaderParser.prototype._parseHeader = function () { + if (this.npairs === this.maxHeaderPairs) { return } + + const lines = this.buffer.split(RE_CRLF) + const len = lines.length + let m, h + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (lines[i].length === 0) { continue } + if (lines[i][0] === '\t' || lines[i][0] === ' ') { + // folded header content + // RFC2822 says to just remove the CRLF and not the whitespace following + // it, so we follow the RFC and include the leading whitespace ... + if (h) { + this.header[h][this.header[h].length - 1] += lines[i] + continue + } + } + + const posColon = lines[i].indexOf(':') + if ( + posColon === -1 || + posColon === 0 + ) { + return + } + m = RE_HDR.exec(lines[i]) + h = m[1].toLowerCase() + this.header[h] = this.header[h] || [] + this.header[h].push((m[2] || '')) + if (++this.npairs === this.maxHeaderPairs) { break } + } +} + +module.exports = HeaderParser diff --git a/node_modules/@fastify/busboy/deps/dicer/lib/PartStream.js b/node_modules/@fastify/busboy/deps/dicer/lib/PartStream.js new file mode 100644 index 0000000..c91da1c --- /dev/null +++ b/node_modules/@fastify/busboy/deps/dicer/lib/PartStream.js @@ -0,0 +1,13 @@ +'use strict' + +const inherits = require('node:util').inherits +const ReadableStream = require('node:stream').Readable + +function PartStream (opts) { + ReadableStream.call(this, opts) +} +inherits(PartStream, ReadableStream) + +PartStream.prototype._read = function (n) {} + +module.exports = PartStream diff --git a/node_modules/@fastify/busboy/deps/dicer/lib/dicer.d.ts b/node_modules/@fastify/busboy/deps/dicer/lib/dicer.d.ts new file mode 100644 index 0000000..3c5b896 --- /dev/null +++ b/node_modules/@fastify/busboy/deps/dicer/lib/dicer.d.ts @@ -0,0 +1,164 @@ +// Type definitions for dicer 0.2 +// Project: https://github.com/mscdex/dicer +// Definitions by: BendingBender <https://github.com/BendingBender> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.2 +/// <reference types="node" /> + +import stream = require("stream"); + +// tslint:disable:unified-signatures + +/** + * A very fast streaming multipart parser for node.js. + * Dicer is a WritableStream + * + * Dicer (special) events: + * - on('finish', ()) - Emitted when all parts have been parsed and the Dicer instance has been ended. + * - on('part', (stream: PartStream)) - Emitted when a new part has been found. + * - on('preamble', (stream: PartStream)) - Emitted for preamble if you should happen to need it (can usually be ignored). + * - on('trailer', (data: Buffer)) - Emitted when trailing data was found after the terminating boundary (as with the preamble, this can usually be ignored too). + */ +export class Dicer extends stream.Writable { + /** + * Creates and returns a new Dicer instance with the following valid config settings: + * + * @param config The configuration to use + */ + constructor(config: Dicer.Config); + /** + * Sets the boundary to use for parsing and performs some initialization needed for parsing. + * You should only need to use this if you set headerFirst to true in the constructor and are parsing the boundary from the preamble header. + * + * @param boundary The boundary to use + */ + setBoundary(boundary: string): void; + addListener(event: "finish", listener: () => void): this; + addListener(event: "part", listener: (stream: Dicer.PartStream) => void): this; + addListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + addListener(event: "trailer", listener: (data: Buffer) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "drain", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "pipe", listener: (src: stream.Readable) => void): this; + addListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + on(event: "finish", listener: () => void): this; + on(event: "part", listener: (stream: Dicer.PartStream) => void): this; + on(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + on(event: "trailer", listener: (data: Buffer) => void): this; + on(event: "close", listener: () => void): this; + on(event: "drain", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "pipe", listener: (src: stream.Readable) => void): this; + on(event: "unpipe", listener: (src: stream.Readable) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: "finish", listener: () => void): this; + once(event: "part", listener: (stream: Dicer.PartStream) => void): this; + once(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + once(event: "trailer", listener: (data: Buffer) => void): this; + once(event: "close", listener: () => void): this; + once(event: "drain", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "pipe", listener: (src: stream.Readable) => void): this; + once(event: "unpipe", listener: (src: stream.Readable) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "finish", listener: () => void): this; + prependListener(event: "part", listener: (stream: Dicer.PartStream) => void): this; + prependListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + prependListener(event: "trailer", listener: (data: Buffer) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "drain", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "finish", listener: () => void): this; + prependOnceListener(event: "part", listener: (stream: Dicer.PartStream) => void): this; + prependOnceListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + prependOnceListener(event: "trailer", listener: (data: Buffer) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "drain", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + removeListener(event: "finish", listener: () => void): this; + removeListener(event: "part", listener: (stream: Dicer.PartStream) => void): this; + removeListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + removeListener(event: "trailer", listener: (data: Buffer) => void): this; + removeListener(event: "close", listener: () => void): this; + removeListener(event: "drain", listener: () => void): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: "pipe", listener: (src: stream.Readable) => void): this; + removeListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; +} + +declare namespace Dicer { + interface Config { + /** + * This is the boundary used to detect the beginning of a new part. + */ + boundary?: string | undefined; + /** + * If true, preamble header parsing will be performed first. + */ + headerFirst?: boolean | undefined; + /** + * The maximum number of header key=>value pairs to parse Default: 2000 (same as node's http). + */ + maxHeaderPairs?: number | undefined; + } + + /** + * PartStream is a _ReadableStream_ + * + * PartStream (special) events: + * - on('header', (header: object)) - An object containing the header for this particular part. Each property value is an array of one or more string values. + */ + interface PartStream extends stream.Readable { + addListener(event: "header", listener: (header: object) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "data", listener: (chunk: Buffer | string) => void): this; + addListener(event: "end", listener: () => void): this; + addListener(event: "readable", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + on(event: "header", listener: (header: object) => void): this; + on(event: "close", listener: () => void): this; + on(event: "data", listener: (chunk: Buffer | string) => void): this; + on(event: "end", listener: () => void): this; + on(event: "readable", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: "header", listener: (header: object) => void): this; + once(event: "close", listener: () => void): this; + once(event: "data", listener: (chunk: Buffer | string) => void): this; + once(event: "end", listener: () => void): this; + once(event: "readable", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "header", listener: (header: object) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "data", listener: (chunk: Buffer | string) => void): this; + prependListener(event: "end", listener: () => void): this; + prependListener(event: "readable", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "header", listener: (header: object) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "data", listener: (chunk: Buffer | string) => void): this; + prependOnceListener(event: "end", listener: () => void): this; + prependOnceListener(event: "readable", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + removeListener(event: "header", listener: (header: object) => void): this; + removeListener(event: "close", listener: () => void): this; + removeListener(event: "data", listener: (chunk: Buffer | string) => void): this; + removeListener(event: "end", listener: () => void): this; + removeListener(event: "readable", listener: () => void): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + } +} \ No newline at end of file diff --git a/node_modules/@fastify/busboy/deps/streamsearch/sbmh.js b/node_modules/@fastify/busboy/deps/streamsearch/sbmh.js new file mode 100644 index 0000000..b90c0e8 --- /dev/null +++ b/node_modules/@fastify/busboy/deps/streamsearch/sbmh.js @@ -0,0 +1,228 @@ +'use strict' + +/** + * Copyright Brian White. All rights reserved. + * + * @see https://github.com/mscdex/streamsearch + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation + * by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool + */ +const EventEmitter = require('node:events').EventEmitter +const inherits = require('node:util').inherits + +function SBMH (needle) { + if (typeof needle === 'string') { + needle = Buffer.from(needle) + } + + if (!Buffer.isBuffer(needle)) { + throw new TypeError('The needle has to be a String or a Buffer.') + } + + const needleLength = needle.length + + if (needleLength === 0) { + throw new Error('The needle cannot be an empty String/Buffer.') + } + + if (needleLength > 256) { + throw new Error('The needle cannot have a length bigger than 256.') + } + + this.maxMatches = Infinity + this.matches = 0 + + this._occ = new Array(256) + .fill(needleLength) // Initialize occurrence table. + this._lookbehind_size = 0 + this._needle = needle + this._bufpos = 0 + + this._lookbehind = Buffer.alloc(needleLength) + + // Populate occurrence table with analysis of the needle, + // ignoring last letter. + for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var + this._occ[needle[i]] = needleLength - 1 - i + } +} +inherits(SBMH, EventEmitter) + +SBMH.prototype.reset = function () { + this._lookbehind_size = 0 + this.matches = 0 + this._bufpos = 0 +} + +SBMH.prototype.push = function (chunk, pos) { + if (!Buffer.isBuffer(chunk)) { + chunk = Buffer.from(chunk, 'binary') + } + const chlen = chunk.length + this._bufpos = pos || 0 + let r + while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) } + return r +} + +SBMH.prototype._sbmh_feed = function (data) { + const len = data.length + const needle = this._needle + const needleLength = needle.length + const lastNeedleChar = needle[needleLength - 1] + + // Positive: points to a position in `data` + // pos == 3 points to data[3] + // Negative: points to a position in the lookbehind buffer + // pos == -2 points to lookbehind[lookbehind_size - 2] + let pos = -this._lookbehind_size + let ch + + if (pos < 0) { + // Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool + // search with character lookup code that considers both the + // lookbehind buffer and the current round's haystack data. + // + // Loop until + // there is a match. + // or until + // we've moved past the position that requires the + // lookbehind buffer. In this case we switch to the + // optimized loop. + // or until + // the character to look at lies outside the haystack. + while (pos < 0 && pos <= len - needleLength) { + ch = this._sbmh_lookup_char(data, pos + needleLength - 1) + + if ( + ch === lastNeedleChar && + this._sbmh_memcmp(data, pos, needleLength - 1) + ) { + this._lookbehind_size = 0 + ++this.matches + this.emit('info', true) + + return (this._bufpos = pos + needleLength) + } + pos += this._occ[ch] + } + + // No match. + + if (pos < 0) { + // There's too few data for Boyer-Moore-Horspool to run, + // so let's use a different algorithm to skip as much as + // we can. + // Forward pos until + // the trailing part of lookbehind + data + // looks like the beginning of the needle + // or until + // pos == 0 + while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos } + } + + if (pos >= 0) { + // Discard lookbehind buffer. + this.emit('info', false, this._lookbehind, 0, this._lookbehind_size) + this._lookbehind_size = 0 + } else { + // Cut off part of the lookbehind buffer that has + // been processed and append the entire haystack + // into it. + const bytesToCutOff = this._lookbehind_size + pos + if (bytesToCutOff > 0) { + // The cut off data is guaranteed not to contain the needle. + this.emit('info', false, this._lookbehind, 0, bytesToCutOff) + } + + this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff, + this._lookbehind_size - bytesToCutOff) + this._lookbehind_size -= bytesToCutOff + + data.copy(this._lookbehind, this._lookbehind_size) + this._lookbehind_size += len + + this._bufpos = len + return len + } + } + + pos += (pos >= 0) * this._bufpos + + // Lookbehind buffer is now empty. We only need to check if the + // needle is in the haystack. + if (data.indexOf(needle, pos) !== -1) { + pos = data.indexOf(needle, pos) + ++this.matches + if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) } + + return (this._bufpos = pos + needleLength) + } else { + pos = len - needleLength + } + + // There was no match. If there's trailing haystack data that we cannot + // match yet using the Boyer-Moore-Horspool algorithm (because the trailing + // data is less than the needle size) then match using a modified + // algorithm that starts matching from the beginning instead of the end. + // Whatever trailing data is left after running this algorithm is added to + // the lookbehind buffer. + while ( + pos < len && + ( + data[pos] !== needle[0] || + ( + (Buffer.compare( + data.subarray(pos, pos + len - pos), + needle.subarray(0, len - pos) + ) !== 0) + ) + ) + ) { + ++pos + } + if (pos < len) { + data.copy(this._lookbehind, 0, pos, pos + (len - pos)) + this._lookbehind_size = len - pos + } + + // Everything until pos is guaranteed not to contain needle data. + if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) } + + this._bufpos = len + return len +} + +SBMH.prototype._sbmh_lookup_char = function (data, pos) { + return (pos < 0) + ? this._lookbehind[this._lookbehind_size + pos] + : data[pos] +} + +SBMH.prototype._sbmh_memcmp = function (data, pos, len) { + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false } + } + return true +} + +module.exports = SBMH diff --git a/node_modules/@fastify/busboy/lib/main.d.ts b/node_modules/@fastify/busboy/lib/main.d.ts new file mode 100644 index 0000000..91b6448 --- /dev/null +++ b/node_modules/@fastify/busboy/lib/main.d.ts @@ -0,0 +1,196 @@ +// Definitions by: Jacob Baskin <https://github.com/jacobbaskin> +// BendingBender <https://github.com/BendingBender> +// Igor Savin <https://github.com/kibertoad> + +/// <reference types="node" /> + +import * as http from 'http'; +import { Readable, Writable } from 'stream'; +export { Dicer } from "../deps/dicer/lib/dicer"; + +export const Busboy: BusboyConstructor; +export default Busboy; + +export interface BusboyConfig { + /** + * These are the HTTP headers of the incoming request, which are used by individual parsers. + */ + headers: BusboyHeaders; + /** + * `highWaterMark` to use for this Busboy instance. + * @default WritableStream default. + */ + highWaterMark?: number | undefined; + /** + * highWaterMark to use for file streams. + * @default ReadableStream default. + */ + fileHwm?: number | undefined; + /** + * Default character set to use when one isn't defined. + * @default 'utf8' + */ + defCharset?: string | undefined; + /** + * Detect if a Part is a file. + * + * By default a file is detected if contentType + * is application/octet-stream or fileName is not + * undefined. + * + * Modify this to handle e.g. Blobs. + */ + isPartAFile?: (fieldName: string | undefined, contentType: string | undefined, fileName: string | undefined) => boolean; + /** + * If paths in the multipart 'filename' field shall be preserved. + * @default false + */ + preservePath?: boolean | undefined; + /** + * Various limits on incoming data. + */ + limits?: + | { + /** + * Max field name size (in bytes) + * @default 100 bytes + */ + fieldNameSize?: number | undefined; + /** + * Max field value size (in bytes) + * @default 1MB + */ + fieldSize?: number | undefined; + /** + * Max number of non-file fields + * @default Infinity + */ + fields?: number | undefined; + /** + * For multipart forms, the max file size (in bytes) + * @default Infinity + */ + fileSize?: number | undefined; + /** + * For multipart forms, the max number of file fields + * @default Infinity + */ + files?: number | undefined; + /** + * For multipart forms, the max number of parts (fields + files) + * @default Infinity + */ + parts?: number | undefined; + /** + * For multipart forms, the max number of header key=>value pairs to parse + * @default 2000 + */ + headerPairs?: number | undefined; + + /** + * For multipart forms, the max size of a header part + * @default 81920 + */ + headerSize?: number | undefined; + } + | undefined; +} + +export type BusboyHeaders = { 'content-type': string } & http.IncomingHttpHeaders; + +export interface BusboyFileStream extends + Readable { + + truncated: boolean; + + /** + * The number of bytes that have been read so far. + */ + bytesRead: number; +} + +export interface Busboy extends Writable { + addListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + + on<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + on(event: string | symbol, listener: (...args: any[]) => void): this; + + once<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + once(event: string | symbol, listener: (...args: any[]) => void): this; + + removeListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + removeListener(event: string | symbol, listener: (...args: any[]) => void): this; + + off<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + off(event: string | symbol, listener: (...args: any[]) => void): this; + + prependListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + + prependOnceListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; +} + +export interface BusboyEvents { + /** + * Emitted for each new file form field found. + * + * * Note: if you listen for this event, you should always handle the `stream` no matter if you care about the + * file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents), + * otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any** + * incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically + * and safely discarded (these discarded files do still count towards `files` and `parts` limits). + * * If a configured file size limit was reached, `stream` will both have a boolean property `truncated` + * (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens. + * + * @param listener.transferEncoding Contains the 'Content-Transfer-Encoding' value for the file stream. + * @param listener.mimeType Contains the 'Content-Type' value for the file stream. + */ + file: ( + fieldname: string, + stream: BusboyFileStream, + filename: string, + transferEncoding: string, + mimeType: string, + ) => void; + /** + * Emitted for each new non-file field found. + */ + field: ( + fieldname: string, + value: string, + fieldnameTruncated: boolean, + valueTruncated: boolean, + transferEncoding: string, + mimeType: string, + ) => void; + finish: () => void; + /** + * Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted. + */ + partsLimit: () => void; + /** + * Emitted when specified `files` limit has been reached. No more 'file' events will be emitted. + */ + filesLimit: () => void; + /** + * Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted. + */ + fieldsLimit: () => void; + error: (error: unknown) => void; +} + +export interface BusboyConstructor { + (options: BusboyConfig): Busboy; + + new(options: BusboyConfig): Busboy; +} + diff --git a/node_modules/@fastify/busboy/lib/main.js b/node_modules/@fastify/busboy/lib/main.js new file mode 100644 index 0000000..8794beb --- /dev/null +++ b/node_modules/@fastify/busboy/lib/main.js @@ -0,0 +1,85 @@ +'use strict' + +const WritableStream = require('node:stream').Writable +const { inherits } = require('node:util') +const Dicer = require('../deps/dicer/lib/Dicer') + +const MultipartParser = require('./types/multipart') +const UrlencodedParser = require('./types/urlencoded') +const parseParams = require('./utils/parseParams') + +function Busboy (opts) { + if (!(this instanceof Busboy)) { return new Busboy(opts) } + + if (typeof opts !== 'object') { + throw new TypeError('Busboy expected an options-Object.') + } + if (typeof opts.headers !== 'object') { + throw new TypeError('Busboy expected an options-Object with headers-attribute.') + } + if (typeof opts.headers['content-type'] !== 'string') { + throw new TypeError('Missing Content-Type-header.') + } + + const { + headers, + ...streamOptions + } = opts + + this.opts = { + autoDestroy: false, + ...streamOptions + } + WritableStream.call(this, this.opts) + + this._done = false + this._parser = this.getParserByHeaders(headers) + this._finished = false +} +inherits(Busboy, WritableStream) + +Busboy.prototype.emit = function (ev) { + if (ev === 'finish') { + if (!this._done) { + this._parser?.end() + return + } else if (this._finished) { + return + } + this._finished = true + } + WritableStream.prototype.emit.apply(this, arguments) +} + +Busboy.prototype.getParserByHeaders = function (headers) { + const parsed = parseParams(headers['content-type']) + + const cfg = { + defCharset: this.opts.defCharset, + fileHwm: this.opts.fileHwm, + headers, + highWaterMark: this.opts.highWaterMark, + isPartAFile: this.opts.isPartAFile, + limits: this.opts.limits, + parsedConType: parsed, + preservePath: this.opts.preservePath + } + + if (MultipartParser.detect.test(parsed[0])) { + return new MultipartParser(this, cfg) + } + if (UrlencodedParser.detect.test(parsed[0])) { + return new UrlencodedParser(this, cfg) + } + throw new Error('Unsupported Content-Type.') +} + +Busboy.prototype._write = function (chunk, encoding, cb) { + this._parser.write(chunk, cb) +} + +module.exports = Busboy +module.exports.default = Busboy +module.exports.Busboy = Busboy + +module.exports.Dicer = Dicer diff --git a/node_modules/@fastify/busboy/lib/types/multipart.js b/node_modules/@fastify/busboy/lib/types/multipart.js new file mode 100644 index 0000000..d691eca --- /dev/null +++ b/node_modules/@fastify/busboy/lib/types/multipart.js @@ -0,0 +1,306 @@ +'use strict' + +// TODO: +// * support 1 nested multipart level +// (see second multipart example here: +// http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data) +// * support limits.fieldNameSize +// -- this will require modifications to utils.parseParams + +const { Readable } = require('node:stream') +const { inherits } = require('node:util') + +const Dicer = require('../../deps/dicer/lib/Dicer') + +const parseParams = require('../utils/parseParams') +const decodeText = require('../utils/decodeText') +const basename = require('../utils/basename') +const getLimit = require('../utils/getLimit') + +const RE_BOUNDARY = /^boundary$/i +const RE_FIELD = /^form-data$/i +const RE_CHARSET = /^charset$/i +const RE_FILENAME = /^filename$/i +const RE_NAME = /^name$/i + +Multipart.detect = /^multipart\/form-data/i +function Multipart (boy, cfg) { + let i + let len + const self = this + let boundary + const limits = cfg.limits + const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)) + const parsedConType = cfg.parsedConType || [] + const defCharset = cfg.defCharset || 'utf8' + const preservePath = cfg.preservePath + const fileOpts = { highWaterMark: cfg.fileHwm } + + for (i = 0, len = parsedConType.length; i < len; ++i) { + if (Array.isArray(parsedConType[i]) && + RE_BOUNDARY.test(parsedConType[i][0])) { + boundary = parsedConType[i][1] + break + } + } + + function checkFinished () { + if (nends === 0 && finished && !boy._done) { + finished = false + self.end() + } + } + + if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') } + + const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + const fileSizeLimit = getLimit(limits, 'fileSize', Infinity) + const filesLimit = getLimit(limits, 'files', Infinity) + const fieldsLimit = getLimit(limits, 'fields', Infinity) + const partsLimit = getLimit(limits, 'parts', Infinity) + const headerPairsLimit = getLimit(limits, 'headerPairs', 2000) + const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024) + + let nfiles = 0 + let nfields = 0 + let nends = 0 + let curFile + let curField + let finished = false + + this._needDrain = false + this._pause = false + this._cb = undefined + this._nparts = 0 + this._boy = boy + + const parserCfg = { + boundary, + maxHeaderPairs: headerPairsLimit, + maxHeaderSize: headerSizeLimit, + partHwm: fileOpts.highWaterMark, + highWaterMark: cfg.highWaterMark + } + + this.parser = new Dicer(parserCfg) + this.parser.on('drain', function () { + self._needDrain = false + if (self._cb && !self._pause) { + const cb = self._cb + self._cb = undefined + cb() + } + }).on('part', function onPart (part) { + if (++self._nparts > partsLimit) { + self.parser.removeListener('part', onPart) + self.parser.on('part', skipPart) + boy.hitPartsLimit = true + boy.emit('partsLimit') + return skipPart(part) + } + + // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let + // us emit 'end' early since we know the part has ended if we are already + // seeing the next part + if (curField) { + const field = curField + field.emit('end') + field.removeAllListeners('end') + } + + part.on('header', function (header) { + let contype + let fieldname + let parsed + let charset + let encoding + let filename + let nsize = 0 + + if (header['content-type']) { + parsed = parseParams(header['content-type'][0]) + if (parsed[0]) { + contype = parsed[0].toLowerCase() + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_CHARSET.test(parsed[i][0])) { + charset = parsed[i][1].toLowerCase() + break + } + } + } + } + + if (contype === undefined) { contype = 'text/plain' } + if (charset === undefined) { charset = defCharset } + + if (header['content-disposition']) { + parsed = parseParams(header['content-disposition'][0]) + if (!RE_FIELD.test(parsed[0])) { return skipPart(part) } + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_NAME.test(parsed[i][0])) { + fieldname = parsed[i][1] + } else if (RE_FILENAME.test(parsed[i][0])) { + filename = parsed[i][1] + if (!preservePath) { filename = basename(filename) } + } + } + } else { return skipPart(part) } + + if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' } + + let onData, + onEnd + + if (isPartAFile(fieldname, contype, filename)) { + // file/binary field + if (nfiles === filesLimit) { + if (!boy.hitFilesLimit) { + boy.hitFilesLimit = true + boy.emit('filesLimit') + } + return skipPart(part) + } + + ++nfiles + + if (boy.listenerCount('file') === 0) { + self.parser._ignore() + return + } + + ++nends + const file = new FileStream(fileOpts) + curFile = file + file.on('end', function () { + --nends + self._pause = false + checkFinished() + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + }) + file._read = function (n) { + if (!self._pause) { return } + self._pause = false + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + } + boy.emit('file', fieldname, file, filename, encoding, contype) + + onData = function (data) { + if ((nsize += data.length) > fileSizeLimit) { + const extralen = fileSizeLimit - nsize + data.length + if (extralen > 0) { file.push(data.slice(0, extralen)) } + file.truncated = true + file.bytesRead = fileSizeLimit + part.removeAllListeners('data') + file.emit('limit') + return + } else if (!file.push(data)) { self._pause = true } + + file.bytesRead = nsize + } + + onEnd = function () { + curFile = undefined + file.push(null) + } + } else { + // non-file field + if (nfields === fieldsLimit) { + if (!boy.hitFieldsLimit) { + boy.hitFieldsLimit = true + boy.emit('fieldsLimit') + } + return skipPart(part) + } + + ++nfields + ++nends + let buffer = '' + let truncated = false + curField = part + + onData = function (data) { + if ((nsize += data.length) > fieldSizeLimit) { + const extralen = (fieldSizeLimit - (nsize - data.length)) + buffer += data.toString('binary', 0, extralen) + truncated = true + part.removeAllListeners('data') + } else { buffer += data.toString('binary') } + } + + onEnd = function () { + curField = undefined + if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) } + boy.emit('field', fieldname, buffer, false, truncated, encoding, contype) + --nends + checkFinished() + } + } + + /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become + broken. Streams2/streams3 is a huge black box of confusion, but + somehow overriding the sync state seems to fix things again (and still + seems to work for previous node versions). + */ + part._readableState.sync = false + + part.on('data', onData) + part.on('end', onEnd) + }).on('error', function (err) { + if (curFile) { curFile.emit('error', err) } + }) + }).on('error', function (err) { + boy.emit('error', err) + }).on('finish', function () { + finished = true + checkFinished() + }) +} + +Multipart.prototype.write = function (chunk, cb) { + const r = this.parser.write(chunk) + if (r && !this._pause) { + cb() + } else { + this._needDrain = !r + this._cb = cb + } +} + +Multipart.prototype.end = function () { + const self = this + + if (self.parser.writable) { + self.parser.end() + } else if (!self._boy._done) { + process.nextTick(function () { + self._boy._done = true + self._boy.emit('finish') + }) + } +} + +function skipPart (part) { + part.resume() +} + +function FileStream (opts) { + Readable.call(this, opts) + + this.bytesRead = 0 + + this.truncated = false +} + +inherits(FileStream, Readable) + +FileStream.prototype._read = function (n) {} + +module.exports = Multipart diff --git a/node_modules/@fastify/busboy/lib/types/urlencoded.js b/node_modules/@fastify/busboy/lib/types/urlencoded.js new file mode 100644 index 0000000..6f5f784 --- /dev/null +++ b/node_modules/@fastify/busboy/lib/types/urlencoded.js @@ -0,0 +1,190 @@ +'use strict' + +const Decoder = require('../utils/Decoder') +const decodeText = require('../utils/decodeText') +const getLimit = require('../utils/getLimit') + +const RE_CHARSET = /^charset$/i + +UrlEncoded.detect = /^application\/x-www-form-urlencoded/i +function UrlEncoded (boy, cfg) { + const limits = cfg.limits + const parsedConType = cfg.parsedConType + this.boy = boy + + this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100) + this.fieldsLimit = getLimit(limits, 'fields', Infinity) + + let charset + for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var + if (Array.isArray(parsedConType[i]) && + RE_CHARSET.test(parsedConType[i][0])) { + charset = parsedConType[i][1].toLowerCase() + break + } + } + + if (charset === undefined) { charset = cfg.defCharset || 'utf8' } + + this.decoder = new Decoder() + this.charset = charset + this._fields = 0 + this._state = 'key' + this._checkingBytes = true + this._bytesKey = 0 + this._bytesVal = 0 + this._key = '' + this._val = '' + this._keyTrunc = false + this._valTrunc = false + this._hitLimit = false +} + +UrlEncoded.prototype.write = function (data, cb) { + if (this._fields === this.fieldsLimit) { + if (!this.boy.hitFieldsLimit) { + this.boy.hitFieldsLimit = true + this.boy.emit('fieldsLimit') + } + return cb() + } + + let idxeq; let idxamp; let i; let p = 0; const len = data.length + + while (p < len) { + if (this._state === 'key') { + idxeq = idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x3D/* = */) { + idxeq = i + break + } else if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesKey } + } + + if (idxeq !== undefined) { + // key with assignment + if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) } + this._state = 'val' + + this._hitLimit = false + this._checkingBytes = true + this._val = '' + this._bytesVal = 0 + this._valTrunc = false + this.decoder.reset() + + p = idxeq + 1 + } else if (idxamp !== undefined) { + // key with no assignment + ++this._fields + let key; const keyTrunc = this._keyTrunc + if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key } + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + if (key.length) { + this.boy.emit('field', decodeText(key, 'binary', this.charset), + '', + keyTrunc, + false) + } + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._keyTrunc = true + } + } else { + if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) } + p = len + } + } else { + idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesVal } + } + + if (idxamp !== undefined) { + ++this._fields + if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) } + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + this._state = 'key' + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._val === '' && this.fieldSizeLimit === 0) || + (this._bytesVal = this._val.length) === this.fieldSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._valTrunc = true + } + } else { + if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) } + p = len + } + } + } + cb() +} + +UrlEncoded.prototype.end = function () { + if (this.boy._done) { return } + + if (this._state === 'key' && this._key.length > 0) { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + '', + this._keyTrunc, + false) + } else if (this._state === 'val') { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + } + this.boy._done = true + this.boy.emit('finish') +} + +module.exports = UrlEncoded diff --git a/node_modules/@fastify/busboy/lib/utils/Decoder.js b/node_modules/@fastify/busboy/lib/utils/Decoder.js new file mode 100644 index 0000000..7917678 --- /dev/null +++ b/node_modules/@fastify/busboy/lib/utils/Decoder.js @@ -0,0 +1,54 @@ +'use strict' + +const RE_PLUS = /\+/g + +const HEX = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +] + +function Decoder () { + this.buffer = undefined +} +Decoder.prototype.write = function (str) { + // Replace '+' with ' ' before decoding + str = str.replace(RE_PLUS, ' ') + let res = '' + let i = 0; let p = 0; const len = str.length + for (; i < len; ++i) { + if (this.buffer !== undefined) { + if (!HEX[str.charCodeAt(i)]) { + res += '%' + this.buffer + this.buffer = undefined + --i // retry character + } else { + this.buffer += str[i] + ++p + if (this.buffer.length === 2) { + res += String.fromCharCode(parseInt(this.buffer, 16)) + this.buffer = undefined + } + } + } else if (str[i] === '%') { + if (i > p) { + res += str.substring(p, i) + p = i + } + this.buffer = '' + ++p + } + } + if (p < len && this.buffer === undefined) { res += str.substring(p) } + return res +} +Decoder.prototype.reset = function () { + this.buffer = undefined +} + +module.exports = Decoder diff --git a/node_modules/@fastify/busboy/lib/utils/basename.js b/node_modules/@fastify/busboy/lib/utils/basename.js new file mode 100644 index 0000000..db58819 --- /dev/null +++ b/node_modules/@fastify/busboy/lib/utils/basename.js @@ -0,0 +1,14 @@ +'use strict' + +module.exports = function basename (path) { + if (typeof path !== 'string') { return '' } + for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var + switch (path.charCodeAt(i)) { + case 0x2F: // '/' + case 0x5C: // '\' + path = path.slice(i + 1) + return (path === '..' || path === '.' ? '' : path) + } + } + return (path === '..' || path === '.' ? '' : path) +} diff --git a/node_modules/@fastify/busboy/lib/utils/decodeText.js b/node_modules/@fastify/busboy/lib/utils/decodeText.js new file mode 100644 index 0000000..eac7d35 --- /dev/null +++ b/node_modules/@fastify/busboy/lib/utils/decodeText.js @@ -0,0 +1,114 @@ +'use strict' + +// Node has always utf-8 +const utf8Decoder = new TextDecoder('utf-8') +const textDecoders = new Map([ + ['utf-8', utf8Decoder], + ['utf8', utf8Decoder] +]) + +function getDecoder (charset) { + let lc + while (true) { + switch (charset) { + case 'utf-8': + case 'utf8': + return decoders.utf8 + case 'latin1': + case 'ascii': // TODO: Make these a separate, strict decoder? + case 'us-ascii': + case 'iso-8859-1': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'windows-1252': + case 'iso_8859-1:1987': + case 'cp1252': + case 'x-cp1252': + return decoders.latin1 + case 'utf16le': + case 'utf-16le': + case 'ucs2': + case 'ucs-2': + return decoders.utf16le + case 'base64': + return decoders.base64 + default: + if (lc === undefined) { + lc = true + charset = charset.toLowerCase() + continue + } + return decoders.other.bind(charset) + } + } +} + +const decoders = { + utf8: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.utf8Slice(0, data.length) + }, + + latin1: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + return data + } + return data.latin1Slice(0, data.length) + }, + + utf16le: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.ucs2Slice(0, data.length) + }, + + base64: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.base64Slice(0, data.length) + }, + + other: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + + if (textDecoders.has(this.toString())) { + try { + return textDecoders.get(this).decode(data) + } catch {} + } + return typeof data === 'string' + ? data + : data.toString() + } +} + +function decodeText (text, sourceEncoding, destEncoding) { + if (text) { + return getDecoder(destEncoding)(text, sourceEncoding) + } + return text +} + +module.exports = decodeText diff --git a/node_modules/@fastify/busboy/lib/utils/getLimit.js b/node_modules/@fastify/busboy/lib/utils/getLimit.js new file mode 100644 index 0000000..cb64fd6 --- /dev/null +++ b/node_modules/@fastify/busboy/lib/utils/getLimit.js @@ -0,0 +1,16 @@ +'use strict' + +module.exports = function getLimit (limits, name, defaultLimit) { + if ( + !limits || + limits[name] === undefined || + limits[name] === null + ) { return defaultLimit } + + if ( + typeof limits[name] !== 'number' || + isNaN(limits[name]) + ) { throw new TypeError('Limit ' + name + ' is not a valid number') } + + return limits[name] +} diff --git a/node_modules/@fastify/busboy/lib/utils/parseParams.js b/node_modules/@fastify/busboy/lib/utils/parseParams.js new file mode 100644 index 0000000..1698e62 --- /dev/null +++ b/node_modules/@fastify/busboy/lib/utils/parseParams.js @@ -0,0 +1,196 @@ +/* eslint-disable object-property-newline */ +'use strict' + +const decodeText = require('./decodeText') + +const RE_ENCODED = /%[a-fA-F0-9][a-fA-F0-9]/g + +const EncodedLookup = { + '%00': '\x00', '%01': '\x01', '%02': '\x02', '%03': '\x03', '%04': '\x04', + '%05': '\x05', '%06': '\x06', '%07': '\x07', '%08': '\x08', '%09': '\x09', + '%0a': '\x0a', '%0A': '\x0a', '%0b': '\x0b', '%0B': '\x0b', '%0c': '\x0c', + '%0C': '\x0c', '%0d': '\x0d', '%0D': '\x0d', '%0e': '\x0e', '%0E': '\x0e', + '%0f': '\x0f', '%0F': '\x0f', '%10': '\x10', '%11': '\x11', '%12': '\x12', + '%13': '\x13', '%14': '\x14', '%15': '\x15', '%16': '\x16', '%17': '\x17', + '%18': '\x18', '%19': '\x19', '%1a': '\x1a', '%1A': '\x1a', '%1b': '\x1b', + '%1B': '\x1b', '%1c': '\x1c', '%1C': '\x1c', '%1d': '\x1d', '%1D': '\x1d', + '%1e': '\x1e', '%1E': '\x1e', '%1f': '\x1f', '%1F': '\x1f', '%20': '\x20', + '%21': '\x21', '%22': '\x22', '%23': '\x23', '%24': '\x24', '%25': '\x25', + '%26': '\x26', '%27': '\x27', '%28': '\x28', '%29': '\x29', '%2a': '\x2a', + '%2A': '\x2a', '%2b': '\x2b', '%2B': '\x2b', '%2c': '\x2c', '%2C': '\x2c', + '%2d': '\x2d', '%2D': '\x2d', '%2e': '\x2e', '%2E': '\x2e', '%2f': '\x2f', + '%2F': '\x2f', '%30': '\x30', '%31': '\x31', '%32': '\x32', '%33': '\x33', + '%34': '\x34', '%35': '\x35', '%36': '\x36', '%37': '\x37', '%38': '\x38', + '%39': '\x39', '%3a': '\x3a', '%3A': '\x3a', '%3b': '\x3b', '%3B': '\x3b', + '%3c': '\x3c', '%3C': '\x3c', '%3d': '\x3d', '%3D': '\x3d', '%3e': '\x3e', + '%3E': '\x3e', '%3f': '\x3f', '%3F': '\x3f', '%40': '\x40', '%41': '\x41', + '%42': '\x42', '%43': '\x43', '%44': '\x44', '%45': '\x45', '%46': '\x46', + '%47': '\x47', '%48': '\x48', '%49': '\x49', '%4a': '\x4a', '%4A': '\x4a', + '%4b': '\x4b', '%4B': '\x4b', '%4c': '\x4c', '%4C': '\x4c', '%4d': '\x4d', + '%4D': '\x4d', '%4e': '\x4e', '%4E': '\x4e', '%4f': '\x4f', '%4F': '\x4f', + '%50': '\x50', '%51': '\x51', '%52': '\x52', '%53': '\x53', '%54': '\x54', + '%55': '\x55', '%56': '\x56', '%57': '\x57', '%58': '\x58', '%59': '\x59', + '%5a': '\x5a', '%5A': '\x5a', '%5b': '\x5b', '%5B': '\x5b', '%5c': '\x5c', + '%5C': '\x5c', '%5d': '\x5d', '%5D': '\x5d', '%5e': '\x5e', '%5E': '\x5e', + '%5f': '\x5f', '%5F': '\x5f', '%60': '\x60', '%61': '\x61', '%62': '\x62', + '%63': '\x63', '%64': '\x64', '%65': '\x65', '%66': '\x66', '%67': '\x67', + '%68': '\x68', '%69': '\x69', '%6a': '\x6a', '%6A': '\x6a', '%6b': '\x6b', + '%6B': '\x6b', '%6c': '\x6c', '%6C': '\x6c', '%6d': '\x6d', '%6D': '\x6d', + '%6e': '\x6e', '%6E': '\x6e', '%6f': '\x6f', '%6F': '\x6f', '%70': '\x70', + '%71': '\x71', '%72': '\x72', '%73': '\x73', '%74': '\x74', '%75': '\x75', + '%76': '\x76', '%77': '\x77', '%78': '\x78', '%79': '\x79', '%7a': '\x7a', + '%7A': '\x7a', '%7b': '\x7b', '%7B': '\x7b', '%7c': '\x7c', '%7C': '\x7c', + '%7d': '\x7d', '%7D': '\x7d', '%7e': '\x7e', '%7E': '\x7e', '%7f': '\x7f', + '%7F': '\x7f', '%80': '\x80', '%81': '\x81', '%82': '\x82', '%83': '\x83', + '%84': '\x84', '%85': '\x85', '%86': '\x86', '%87': '\x87', '%88': '\x88', + '%89': '\x89', '%8a': '\x8a', '%8A': '\x8a', '%8b': '\x8b', '%8B': '\x8b', + '%8c': '\x8c', '%8C': '\x8c', '%8d': '\x8d', '%8D': '\x8d', '%8e': '\x8e', + '%8E': '\x8e', '%8f': '\x8f', '%8F': '\x8f', '%90': '\x90', '%91': '\x91', + '%92': '\x92', '%93': '\x93', '%94': '\x94', '%95': '\x95', '%96': '\x96', + '%97': '\x97', '%98': '\x98', '%99': '\x99', '%9a': '\x9a', '%9A': '\x9a', + '%9b': '\x9b', '%9B': '\x9b', '%9c': '\x9c', '%9C': '\x9c', '%9d': '\x9d', + '%9D': '\x9d', '%9e': '\x9e', '%9E': '\x9e', '%9f': '\x9f', '%9F': '\x9f', + '%a0': '\xa0', '%A0': '\xa0', '%a1': '\xa1', '%A1': '\xa1', '%a2': '\xa2', + '%A2': '\xa2', '%a3': '\xa3', '%A3': '\xa3', '%a4': '\xa4', '%A4': '\xa4', + '%a5': '\xa5', '%A5': '\xa5', '%a6': '\xa6', '%A6': '\xa6', '%a7': '\xa7', + '%A7': '\xa7', '%a8': '\xa8', '%A8': '\xa8', '%a9': '\xa9', '%A9': '\xa9', + '%aa': '\xaa', '%Aa': '\xaa', '%aA': '\xaa', '%AA': '\xaa', '%ab': '\xab', + '%Ab': '\xab', '%aB': '\xab', '%AB': '\xab', '%ac': '\xac', '%Ac': '\xac', + '%aC': '\xac', '%AC': '\xac', '%ad': '\xad', '%Ad': '\xad', '%aD': '\xad', + '%AD': '\xad', '%ae': '\xae', '%Ae': '\xae', '%aE': '\xae', '%AE': '\xae', + '%af': '\xaf', '%Af': '\xaf', '%aF': '\xaf', '%AF': '\xaf', '%b0': '\xb0', + '%B0': '\xb0', '%b1': '\xb1', '%B1': '\xb1', '%b2': '\xb2', '%B2': '\xb2', + '%b3': '\xb3', '%B3': '\xb3', '%b4': '\xb4', '%B4': '\xb4', '%b5': '\xb5', + '%B5': '\xb5', '%b6': '\xb6', '%B6': '\xb6', '%b7': '\xb7', '%B7': '\xb7', + '%b8': '\xb8', '%B8': '\xb8', '%b9': '\xb9', '%B9': '\xb9', '%ba': '\xba', + '%Ba': '\xba', '%bA': '\xba', '%BA': '\xba', '%bb': '\xbb', '%Bb': '\xbb', + '%bB': '\xbb', '%BB': '\xbb', '%bc': '\xbc', '%Bc': '\xbc', '%bC': '\xbc', + '%BC': '\xbc', '%bd': '\xbd', '%Bd': '\xbd', '%bD': '\xbd', '%BD': '\xbd', + '%be': '\xbe', '%Be': '\xbe', '%bE': '\xbe', '%BE': '\xbe', '%bf': '\xbf', + '%Bf': '\xbf', '%bF': '\xbf', '%BF': '\xbf', '%c0': '\xc0', '%C0': '\xc0', + '%c1': '\xc1', '%C1': '\xc1', '%c2': '\xc2', '%C2': '\xc2', '%c3': '\xc3', + '%C3': '\xc3', '%c4': '\xc4', '%C4': '\xc4', '%c5': '\xc5', '%C5': '\xc5', + '%c6': '\xc6', '%C6': '\xc6', '%c7': '\xc7', '%C7': '\xc7', '%c8': '\xc8', + '%C8': '\xc8', '%c9': '\xc9', '%C9': '\xc9', '%ca': '\xca', '%Ca': '\xca', + '%cA': '\xca', '%CA': '\xca', '%cb': '\xcb', '%Cb': '\xcb', '%cB': '\xcb', + '%CB': '\xcb', '%cc': '\xcc', '%Cc': '\xcc', '%cC': '\xcc', '%CC': '\xcc', + '%cd': '\xcd', '%Cd': '\xcd', '%cD': '\xcd', '%CD': '\xcd', '%ce': '\xce', + '%Ce': '\xce', '%cE': '\xce', '%CE': '\xce', '%cf': '\xcf', '%Cf': '\xcf', + '%cF': '\xcf', '%CF': '\xcf', '%d0': '\xd0', '%D0': '\xd0', '%d1': '\xd1', + '%D1': '\xd1', '%d2': '\xd2', '%D2': '\xd2', '%d3': '\xd3', '%D3': '\xd3', + '%d4': '\xd4', '%D4': '\xd4', '%d5': '\xd5', '%D5': '\xd5', '%d6': '\xd6', + '%D6': '\xd6', '%d7': '\xd7', '%D7': '\xd7', '%d8': '\xd8', '%D8': '\xd8', + '%d9': '\xd9', '%D9': '\xd9', '%da': '\xda', '%Da': '\xda', '%dA': '\xda', + '%DA': '\xda', '%db': '\xdb', '%Db': '\xdb', '%dB': '\xdb', '%DB': '\xdb', + '%dc': '\xdc', '%Dc': '\xdc', '%dC': '\xdc', '%DC': '\xdc', '%dd': '\xdd', + '%Dd': '\xdd', '%dD': '\xdd', '%DD': '\xdd', '%de': '\xde', '%De': '\xde', + '%dE': '\xde', '%DE': '\xde', '%df': '\xdf', '%Df': '\xdf', '%dF': '\xdf', + '%DF': '\xdf', '%e0': '\xe0', '%E0': '\xe0', '%e1': '\xe1', '%E1': '\xe1', + '%e2': '\xe2', '%E2': '\xe2', '%e3': '\xe3', '%E3': '\xe3', '%e4': '\xe4', + '%E4': '\xe4', '%e5': '\xe5', '%E5': '\xe5', '%e6': '\xe6', '%E6': '\xe6', + '%e7': '\xe7', '%E7': '\xe7', '%e8': '\xe8', '%E8': '\xe8', '%e9': '\xe9', + '%E9': '\xe9', '%ea': '\xea', '%Ea': '\xea', '%eA': '\xea', '%EA': '\xea', + '%eb': '\xeb', '%Eb': '\xeb', '%eB': '\xeb', '%EB': '\xeb', '%ec': '\xec', + '%Ec': '\xec', '%eC': '\xec', '%EC': '\xec', '%ed': '\xed', '%Ed': '\xed', + '%eD': '\xed', '%ED': '\xed', '%ee': '\xee', '%Ee': '\xee', '%eE': '\xee', + '%EE': '\xee', '%ef': '\xef', '%Ef': '\xef', '%eF': '\xef', '%EF': '\xef', + '%f0': '\xf0', '%F0': '\xf0', '%f1': '\xf1', '%F1': '\xf1', '%f2': '\xf2', + '%F2': '\xf2', '%f3': '\xf3', '%F3': '\xf3', '%f4': '\xf4', '%F4': '\xf4', + '%f5': '\xf5', '%F5': '\xf5', '%f6': '\xf6', '%F6': '\xf6', '%f7': '\xf7', + '%F7': '\xf7', '%f8': '\xf8', '%F8': '\xf8', '%f9': '\xf9', '%F9': '\xf9', + '%fa': '\xfa', '%Fa': '\xfa', '%fA': '\xfa', '%FA': '\xfa', '%fb': '\xfb', + '%Fb': '\xfb', '%fB': '\xfb', '%FB': '\xfb', '%fc': '\xfc', '%Fc': '\xfc', + '%fC': '\xfc', '%FC': '\xfc', '%fd': '\xfd', '%Fd': '\xfd', '%fD': '\xfd', + '%FD': '\xfd', '%fe': '\xfe', '%Fe': '\xfe', '%fE': '\xfe', '%FE': '\xfe', + '%ff': '\xff', '%Ff': '\xff', '%fF': '\xff', '%FF': '\xff' +} + +function encodedReplacer (match) { + return EncodedLookup[match] +} + +const STATE_KEY = 0 +const STATE_VALUE = 1 +const STATE_CHARSET = 2 +const STATE_LANG = 3 + +function parseParams (str) { + const res = [] + let state = STATE_KEY + let charset = '' + let inquote = false + let escaping = false + let p = 0 + let tmp = '' + const len = str.length + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + const char = str[i] + if (char === '\\' && inquote) { + if (escaping) { escaping = false } else { + escaping = true + continue + } + } else if (char === '"') { + if (!escaping) { + if (inquote) { + inquote = false + state = STATE_KEY + } else { inquote = true } + continue + } else { escaping = false } + } else { + if (escaping && inquote) { tmp += '\\' } + escaping = false + if ((state === STATE_CHARSET || state === STATE_LANG) && char === "'") { + if (state === STATE_CHARSET) { + state = STATE_LANG + charset = tmp.substring(1) + } else { state = STATE_VALUE } + tmp = '' + continue + } else if (state === STATE_KEY && + (char === '*' || char === '=') && + res.length) { + state = char === '*' + ? STATE_CHARSET + : STATE_VALUE + res[p] = [tmp, undefined] + tmp = '' + continue + } else if (!inquote && char === ';') { + state = STATE_KEY + if (charset) { + if (tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } + charset = '' + } else if (tmp.length) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp } + tmp = '' + ++p + continue + } else if (!inquote && (char === ' ' || char === '\t')) { continue } + } + tmp += char + } + if (charset && tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } else if (tmp) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + + if (res[p] === undefined) { + if (tmp) { res[p] = tmp } + } else { res[p][1] = tmp } + + return res +} + +module.exports = parseParams diff --git a/node_modules/@fastify/busboy/package.json b/node_modules/@fastify/busboy/package.json new file mode 100644 index 0000000..83693ac --- /dev/null +++ b/node_modules/@fastify/busboy/package.json @@ -0,0 +1,86 @@ +{ + "name": "@fastify/busboy", + "version": "2.1.1", + "private": false, + "author": "Brian White <mscdex@mscdex.net>", + "contributors": [ + { + "name": "Igor Savin", + "email": "kibertoad@gmail.com", + "url": "https://github.com/kibertoad" + }, + { + "name": "Aras Abbasi", + "email": "aras.abbasi@gmail.com", + "url": "https://github.com/uzlopak" + } + ], + "description": "A streaming parser for HTML form data for node.js", + "main": "lib/main", + "type": "commonjs", + "types": "lib/main.d.ts", + "scripts": { + "bench:busboy": "cd benchmarks && npm install && npm run benchmark-fastify", + "bench:dicer": "node bench/dicer/dicer-bench-multipart-parser.js", + "coveralls": "nyc report --reporter=lcov", + "lint": "npm run lint:standard", + "lint:everything": "npm run lint && npm run test:types", + "lint:fix": "standard --fix", + "lint:standard": "standard --verbose | snazzy", + "test:mocha": "tap", + "test:types": "tsd", + "test:coverage": "nyc npm run test", + "test": "npm run test:mocha" + }, + "engines": { + "node": ">=14" + }, + "devDependencies": { + "@types/node": "^20.1.0", + "busboy": "^1.0.0", + "photofinish": "^1.8.0", + "snazzy": "^9.0.0", + "standard": "^17.0.0", + "tap": "^16.3.8", + "tinybench": "^2.5.1", + "tsd": "^0.30.0", + "typescript": "^5.0.2" + }, + "keywords": [ + "uploads", + "forms", + "multipart", + "form-data" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/fastify/busboy.git" + }, + "tsd": { + "directory": "test/types", + "compilerOptions": { + "esModuleInterop": false, + "module": "commonjs", + "target": "ES2017" + } + }, + "standard": { + "globals": [ + "describe", + "it" + ], + "ignore": [ + "bench" + ] + }, + "files": [ + "README.md", + "LICENSE", + "lib/*", + "deps/encoding/*", + "deps/dicer/lib", + "deps/streamsearch/", + "deps/dicer/LICENSE" + ] +} diff --git a/node_modules/express-rate-limit/dist/index.cjs b/node_modules/express-rate-limit/dist/index.cjs new file mode 100644 index 0000000..36d5e1b --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.cjs @@ -0,0 +1,838 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// source/index.ts +var source_exports = {}; +__export(source_exports, { + MemoryStore: () => MemoryStore, + default: () => lib_default, + rateLimit: () => lib_default +}); +module.exports = __toCommonJS(source_exports); + +// source/headers.ts +var import_node_buffer = require("buffer"); +var import_node_crypto = require("crypto"); +var SUPPORTED_DRAFT_VERSIONS = ["draft-6", "draft-7", "draft-8"]; +var getResetSeconds = (resetTime, windowMs) => { + let resetSeconds = void 0; + if (resetTime) { + const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3); + resetSeconds = Math.max(0, deltaSeconds); + } else if (windowMs) { + resetSeconds = Math.ceil(windowMs / 1e3); + } + return resetSeconds; +}; +var getPartitionKey = (key) => { + const hash = (0, import_node_crypto.createHash)("sha256"); + hash.update(key); + const partitionKey = hash.digest("hex").slice(0, 12); + return import_node_buffer.Buffer.from(partitionKey).toString("base64"); +}; +var setLegacyHeaders = (response, info) => { + if (response.headersSent) + return; + response.setHeader("X-RateLimit-Limit", info.limit.toString()); + response.setHeader("X-RateLimit-Remaining", info.remaining.toString()); + if (info.resetTime instanceof Date) { + response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString()); + response.setHeader( + "X-RateLimit-Reset", + Math.ceil(info.resetTime.getTime() / 1e3).toString() + ); + } +}; +var setDraft6Headers = (response, info, windowMs) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime); + response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`); + response.setHeader("RateLimit-Limit", info.limit.toString()); + response.setHeader("RateLimit-Remaining", info.remaining.toString()); + if (resetSeconds) + response.setHeader("RateLimit-Reset", resetSeconds.toString()); +}; +var setDraft7Headers = (response, info, windowMs) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`); + response.setHeader( + "RateLimit", + `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}` + ); +}; +var setDraft8Headers = (response, info, windowMs, name, key) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + const partitionKey = getPartitionKey(key); + const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`; + const header = `r=${info.remaining}; t=${resetSeconds}`; + response.append("RateLimit-Policy", `"${name}"; ${policy}`); + response.append("RateLimit", `"${name}"; ${header}`); +}; +var setRetryAfterHeader = (response, info, windowMs) => { + if (response.headersSent) + return; + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + response.setHeader("Retry-After", resetSeconds.toString()); +}; + +// source/validations.ts +var import_node_net = require("net"); +var ValidationError = class extends Error { + /** + * The code must be a string, in snake case and all capital, that starts with + * the substring `ERR_ERL_`. + * + * The message must be a string, starting with an uppercase character, + * describing the issue in detail. + */ + constructor(code, message) { + const url = `https://express-rate-limit.github.io/${code}/`; + super(`${message} See ${url} for more information.`); + this.name = this.constructor.name; + this.code = code; + this.help = url; + } +}; +var ChangeWarning = class extends ValidationError { +}; +var usedStores = /* @__PURE__ */ new Set(); +var singleCountKeys = /* @__PURE__ */ new WeakMap(); +var validations = { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + enabled: { + default: true + }, + // Should be EnabledValidations type, but that's a circular reference + disable() { + for (const k of Object.keys(this.enabled)) + this.enabled[k] = false; + }, + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip) { + if (ip === void 0) { + throw new ValidationError( + "ERR_ERL_UNDEFINED_IP_ADDRESS", + `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.` + ); + } + if (!(0, import_node_net.isIP)(ip)) { + throw new ValidationError( + "ERR_ERL_INVALID_IP_ADDRESS", + `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.` + ); + } + }, + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request) { + if (request.app.get("trust proxy") === true) { + throw new ValidationError( + "ERR_ERL_PERMISSIVE_TRUST_PROXY", + `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.` + ); + } + }, + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request) { + if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) { + throw new ValidationError( + "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR", + `The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.` + ); + } + }, + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits) { + if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) { + throw new ValidationError( + "ERR_ERL_INVALID_HITS", + `The totalHits value returned from the store must be a positive integer, got ${hits}` + ); + } + }, + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store) { + if (usedStores.has(store)) { + const maybeUniquePrefix = store?.localKeys ? "" : " (with a unique prefix)"; + throw new ValidationError( + "ERR_ERL_STORE_REUSE", + `A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.` + ); + } + usedStores.add(store); + }, + /** + * Ensures a given key is incremented only once per request. + * + * @param request {Request} - The Express request object. + * @param store {Store} - The store class. + * @param key {string} - The key used to store the client's hit count. + * + * @returns {void} + */ + singleCount(request, store, key) { + let storeKeys = singleCountKeys.get(request); + if (!storeKeys) { + storeKeys = /* @__PURE__ */ new Map(); + singleCountKeys.set(request, storeKeys); + } + const storeKey = store.localKeys ? store : store.constructor.name; + let keys = storeKeys.get(storeKey); + if (!keys) { + keys = []; + storeKeys.set(storeKey, keys); + } + const prefixedKey = `${store.prefix ?? ""}${key}`; + if (keys.includes(prefixedKey)) { + throw new ValidationError( + "ERR_ERL_DOUBLE_COUNT", + `The hit count for ${key} was incremented more than once for a single request.` + ); + } + keys.push(prefixedKey); + }, + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit) { + if (limit === 0) { + throw new ChangeWarning( + "WRN_ERL_MAX_ZERO", + `Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7` + ); + } + }, + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers) { + if (draft_polli_ratelimit_headers) { + throw new ChangeWarning( + "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS", + `The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.` + ); + } + }, + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached) { + if (onLimitReached) { + throw new ChangeWarning( + "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED", + `The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.` + ); + } + }, + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version) { + if (typeof version !== "string" || !SUPPORTED_DRAFT_VERSIONS.includes(version)) { + const versionString = SUPPORTED_DRAFT_VERSIONS.join(", "); + throw new ValidationError( + "ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION", + `standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.` + ); + } + }, + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime) { + if (!resetTime) { + throw new ValidationError( + "ERR_ERL_HEADERS_NO_RESET", + `standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.` + ); + } + }, + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig() { + const supportedValidations = Object.keys(this).filter( + (k) => !["enabled", "disable"].includes(k) + ); + supportedValidations.push("default"); + for (const key of Object.keys(this.enabled)) { + if (!supportedValidations.includes(key)) { + throw new ValidationError( + "ERR_ERL_UNKNOWN_VALIDATION", + `options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join( + ", " + )}.` + ); + } + } + }, + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store) { + const { stack } = new Error( + "express-rate-limit validation check (set options.validate.creationStack=false to disable)" + ); + if (stack?.includes("Layer.handle [as handle_request]")) { + if (!store.localKeys) { + throw new ValidationError( + "ERR_ERL_CREATED_IN_REQUEST_HANDLER", + "express-rate-limit instance should *usually* be created at app initialization, not when responding to a request." + ); + } + throw new ValidationError( + "ERR_ERL_CREATED_IN_REQUEST_HANDLER", + `express-rate-limit instance should be created at app initialization, not when responding to a request.` + ); + } + } +}; +var getValidations = (_enabled) => { + let enabled; + if (typeof _enabled === "boolean") { + enabled = { + default: _enabled + }; + } else { + enabled = { + default: true, + ..._enabled + }; + } + const wrappedValidations = { + enabled + }; + for (const [name, validation] of Object.entries(validations)) { + if (typeof validation === "function") + wrappedValidations[name] = (...args) => { + if (!(enabled[name] ?? enabled.default)) { + return; + } + try { + ; + validation.apply( + wrappedValidations, + args + ); + } catch (error) { + if (error instanceof ChangeWarning) + console.warn(error); + else + console.error(error); + } + }; + } + return wrappedValidations; +}; + +// source/memory-store.ts +var MemoryStore = class { + constructor() { + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + this.previous = /* @__PURE__ */ new Map(); + this.current = /* @__PURE__ */ new Map(); + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + this.localKeys = true; + } + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options) { + this.windowMs = options.windowMs; + if (this.interval) + clearInterval(this.interval); + this.interval = setInterval(() => { + this.clearExpired(); + }, this.windowMs); + if (this.interval.unref) + this.interval.unref(); + } + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + async get(key) { + return this.current.get(key) ?? this.previous.get(key); + } + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + async increment(key) { + const client = this.getClient(key); + const now = Date.now(); + if (client.resetTime.getTime() <= now) { + this.resetClient(client, now); + } + client.totalHits++; + return client; + } + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + async decrement(key) { + const client = this.getClient(key); + if (client.totalHits > 0) + client.totalHits--; + } + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + async resetKey(key) { + this.current.delete(key); + this.previous.delete(key); + } + /** + * Method to reset everyone's hit counter. + * + * @public + */ + async resetAll() { + this.current.clear(); + this.previous.clear(); + } + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown() { + clearInterval(this.interval); + void this.resetAll(); + } + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + resetClient(client, now = Date.now()) { + client.totalHits = 0; + client.resetTime.setTime(now + this.windowMs); + return client; + } + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + getClient(key) { + if (this.current.has(key)) + return this.current.get(key); + let client; + if (this.previous.has(key)) { + client = this.previous.get(key); + this.previous.delete(key); + } else { + client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() }; + this.resetClient(client); + } + this.current.set(key, client); + return client; + } + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + clearExpired() { + this.previous = this.current; + this.current = /* @__PURE__ */ new Map(); + } +}; + +// source/lib.ts +var isLegacyStore = (store) => ( + // Check that `incr` exists but `increment` does not - store authors might want + // to keep both around for backwards compatibility. + typeof store.incr === "function" && typeof store.increment !== "function" +); +var promisifyStore = (passedStore) => { + if (!isLegacyStore(passedStore)) { + return passedStore; + } + const legacyStore = passedStore; + class PromisifiedStore { + async increment(key) { + return new Promise((resolve, reject) => { + legacyStore.incr( + key, + (error, totalHits, resetTime) => { + if (error) + reject(error); + resolve({ totalHits, resetTime }); + } + ); + }); + } + async decrement(key) { + return legacyStore.decrement(key); + } + async resetKey(key) { + return legacyStore.resetKey(key); + } + /* istanbul ignore next */ + async resetAll() { + if (typeof legacyStore.resetAll === "function") + return legacyStore.resetAll(); + } + } + return new PromisifiedStore(); +}; +var getOptionsFromConfig = (config) => { + const { validations: validations2, ...directlyPassableEntries } = config; + return { + ...directlyPassableEntries, + validate: validations2.enabled + }; +}; +var omitUndefinedOptions = (passedOptions) => { + const omittedOptions = {}; + for (const k of Object.keys(passedOptions)) { + const key = k; + if (passedOptions[key] !== void 0) { + omittedOptions[key] = passedOptions[key]; + } + } + return omittedOptions; +}; +var parseOptions = (passedOptions) => { + const notUndefinedOptions = omitUndefinedOptions(passedOptions); + const validations2 = getValidations(notUndefinedOptions?.validate ?? true); + validations2.validationsConfig(); + validations2.draftPolliHeaders( + // @ts-expect-error see the note above. + notUndefinedOptions.draft_polli_ratelimit_headers + ); + validations2.onLimitReached(notUndefinedOptions.onLimitReached); + let standardHeaders = notUndefinedOptions.standardHeaders ?? false; + if (standardHeaders === true) + standardHeaders = "draft-6"; + const config = { + windowMs: 60 * 1e3, + limit: passedOptions.max ?? 5, + // `max` is deprecated, but support it anyways. + message: "Too many requests, please try again later.", + statusCode: 429, + legacyHeaders: passedOptions.headers ?? true, + identifier(request, _response) { + let duration = ""; + const property = config.requestPropertyName; + const { limit } = request[property]; + const seconds = config.windowMs / 1e3; + const minutes = config.windowMs / (1e3 * 60); + const hours = config.windowMs / (1e3 * 60 * 60); + const days = config.windowMs / (1e3 * 60 * 60 * 24); + if (seconds < 60) + duration = `${seconds}sec`; + else if (minutes < 60) + duration = `${minutes}min`; + else if (hours < 24) + duration = `${hours}hr${hours > 1 ? "s" : ""}`; + else + duration = `${days}day${days > 1 ? "s" : ""}`; + return `${limit}-in-${duration}`; + }, + requestPropertyName: "rateLimit", + skipFailedRequests: false, + skipSuccessfulRequests: false, + requestWasSuccessful: (_request, response) => response.statusCode < 400, + skip: (_request, _response) => false, + keyGenerator(request, _response) { + validations2.ip(request.ip); + validations2.trustProxy(request); + validations2.xForwardedForHeader(request); + return request.ip; + }, + async handler(request, response, _next, _optionsUsed) { + response.status(config.statusCode); + const message = typeof config.message === "function" ? await config.message( + request, + response + ) : config.message; + if (!response.writableEnded) { + response.send(message); + } + }, + passOnStoreError: false, + // Allow the default options to be overriden by the passed options. + ...notUndefinedOptions, + // `standardHeaders` is resolved into a draft version above, use that. + standardHeaders, + // Note that this field is declared after the user's options are spread in, + // so that this field doesn't get overriden with an un-promisified store! + store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()), + // Print an error to the console if a few known misconfigurations are detected. + validations: validations2 + }; + if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || config.store.resetAll !== void 0 && typeof config.store.resetAll !== "function" || config.store.init !== void 0 && typeof config.store.init !== "function") { + throw new TypeError( + "An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface." + ); + } + return config; +}; +var handleAsyncErrors = (fn) => async (request, response, next) => { + try { + await Promise.resolve(fn(request, response, next)).catch(next); + } catch (error) { + next(error); + } +}; +var rateLimit = (passedOptions) => { + const config = parseOptions(passedOptions ?? {}); + const options = getOptionsFromConfig(config); + config.validations.creationStack(config.store); + config.validations.unsharedStore(config.store); + if (typeof config.store.init === "function") + config.store.init(options); + const middleware = handleAsyncErrors( + async (request, response, next) => { + const skip = await config.skip(request, response); + if (skip) { + next(); + return; + } + const augmentedRequest = request; + const key = await config.keyGenerator(request, response); + let totalHits = 0; + let resetTime; + try { + const incrementResult = await config.store.increment(key); + totalHits = incrementResult.totalHits; + resetTime = incrementResult.resetTime; + } catch (error) { + if (config.passOnStoreError) { + console.error( + "express-rate-limit: error from store, allowing request without rate-limiting.", + error + ); + next(); + return; + } + throw error; + } + config.validations.positiveHits(totalHits); + config.validations.singleCount(request, config.store, key); + const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit; + const limit = await retrieveLimit; + config.validations.limit(limit); + const info = { + limit, + used: totalHits, + remaining: Math.max(limit - totalHits, 0), + resetTime + }; + Object.defineProperty(info, "current", { + configurable: false, + enumerable: false, + value: totalHits + }); + augmentedRequest[config.requestPropertyName] = info; + if (config.legacyHeaders && !response.headersSent) { + setLegacyHeaders(response, info); + } + if (config.standardHeaders && !response.headersSent) { + switch (config.standardHeaders) { + case "draft-6": { + setDraft6Headers(response, info, config.windowMs); + break; + } + case "draft-7": { + config.validations.headersResetTime(info.resetTime); + setDraft7Headers(response, info, config.windowMs); + break; + } + case "draft-8": { + const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier; + const name = await retrieveName; + config.validations.headersResetTime(info.resetTime); + setDraft8Headers(response, info, config.windowMs, name, key); + break; + } + default: { + config.validations.headersDraftVersion(config.standardHeaders); + break; + } + } + } + if (config.skipFailedRequests || config.skipSuccessfulRequests) { + let decremented = false; + const decrementKey = async () => { + if (!decremented) { + await config.store.decrement(key); + decremented = true; + } + }; + if (config.skipFailedRequests) { + response.on("finish", async () => { + if (!await config.requestWasSuccessful(request, response)) + await decrementKey(); + }); + response.on("close", async () => { + if (!response.writableEnded) + await decrementKey(); + }); + response.on("error", async () => { + await decrementKey(); + }); + } + if (config.skipSuccessfulRequests) { + response.on("finish", async () => { + if (await config.requestWasSuccessful(request, response)) + await decrementKey(); + }); + } + } + config.validations.disable(); + if (totalHits > limit) { + if (config.legacyHeaders || config.standardHeaders) { + setRetryAfterHeader(response, info, config.windowMs); + } + config.handler(request, response, next, options); + return; + } + next(); + } + ); + const getThrowFn = () => { + throw new Error("The current store does not support the get/getKey method"); + }; + middleware.resetKey = config.store.resetKey.bind(config.store); + middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn; + return middleware; +}; +var lib_default = rateLimit; +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + MemoryStore, + rateLimit +}); +module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore; diff --git a/node_modules/express-rate-limit/dist/index.d.cts b/node_modules/express-rate-limit/dist/index.d.cts new file mode 100644 index 0000000..62fe123 --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.d.cts @@ -0,0 +1,584 @@ +// Generated by dts-bundle-generator v8.0.1 + +import { NextFunction, Request, RequestHandler, Response } from 'express'; + +declare const validations: { + enabled: { + [key: string]: boolean; + }; + disable(): void; + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip: string | undefined): void; + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request: Request): void; + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request: Request): void; + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits: any): void; + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store: Store): void; + /** + * Ensures a given key is incremented only once per request. + * + * @param request {Request} - The Express request object. + * @param store {Store} - The store class. + * @param key {string} - The key used to store the client's hit count. + * + * @returns {void} + */ + singleCount(request: Request, store: Store, key: string): void; + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit: number): void; + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers?: any): void; + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached?: any): void; + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version?: any): void; + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime?: Date): void; + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig(): void; + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store: Store): void; +}; +export type Validations = typeof validations; +declare const SUPPORTED_DRAFT_VERSIONS: string[]; +/** + * Callback that fires when a client's hit counter is incremented. + * + * @param error {Error | undefined} - The error that occurred, if any. + * @param totalHits {number} - The number of hits for that client so far. + * @param resetTime {Date | undefined} - The time when the counter resets. + */ +export type IncrementCallback = (error: Error | undefined, totalHits: number, resetTime: Date | undefined) => void; +/** + * Method (in the form of middleware) to generate/retrieve a value based on the + * incoming request. + * + * @param request {Request} - The Express request object. + * @param response {Response} - The Express response object. + * + * @returns {T} - The value needed. + */ +export type ValueDeterminingMiddleware<T> = (request: Request, response: Response) => T | Promise<T>; +/** + * Express request handler that sends back a response when a client is + * rate-limited. + * + * @param request {Request} - The Express request object. + * @param response {Response} - The Express response object. + * @param next {NextFunction} - The Express `next` function, can be called to skip responding. + * @param optionsUsed {Options} - The options used to set up the middleware. + */ +export type RateLimitExceededEventHandler = (request: Request, response: Response, next: NextFunction, optionsUsed: Options) => void; +/** + * Event callback that is triggered on a client's first request that exceeds the limit + * but not for subsequent requests. May be used for logging, etc. Should *not* + * send a response. + * + * @param request {Request} - The Express request object. + * @param response {Response} - The Express response object. + * @param optionsUsed {Options} - The options used to set up the middleware. + */ +export type RateLimitReachedEventHandler = (request: Request, response: Response, optionsUsed: Options) => void; +/** + * Data returned from the `Store` when a client's hit counter is incremented. + * + * @property totalHits {number} - The number of hits for that client so far. + * @property resetTime {Date | undefined} - The time when the counter resets. + */ +export type ClientRateLimitInfo = { + totalHits: number; + resetTime: Date | undefined; +}; +export type IncrementResponse = ClientRateLimitInfo; +/** + * A modified Express request handler with the rate limit functions. + */ +export type RateLimitRequestHandler = RequestHandler & { + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + getKey: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined; +}; +/** + * An interface that all hit counter stores must implement. + * + * @deprecated 6.x - Implement the `Store` interface instead. + */ +export type LegacyStore = { + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * @param callback {IncrementCallback} - The callback to call once the counter is incremented. + */ + incr: (key: string, callback: IncrementCallback) => void; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + decrement: (key: string) => void; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => void; + /** + * Method to reset everyone's hit counter. + */ + resetAll?: () => void; +}; +/** + * An interface that all hit counter stores must implement. + */ +export type Store = { + /** + * Method that initializes the store, and has access to the options passed to + * the middleware too. + * + * @param options {Options} - The options used to setup the middleware. + */ + init?: (options: Options) => void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client. + */ + increment: (key: string) => Promise<IncrementResponse> | IncrementResponse; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + decrement: (key: string) => Promise<void> | void; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => Promise<void> | void; + /** + * Method to reset everyone's hit counter. + */ + resetAll?: () => Promise<void> | void; + /** + * Method to shutdown the store, stop timers, and release all resources. + */ + shutdown?: () => Promise<void> | void; + /** + * Flag to indicate that keys incremented in one instance of this store can + * not affect other instances. Typically false if a database is used, true for + * MemoryStore. + * + * Used to help detect double-counting misconfigurations. + */ + localKeys?: boolean; + /** + * Optional value that the store prepends to keys + * + * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes + */ + prefix?: string; +}; +export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number]; +/** + * Validate configuration object for enabling or disabling specific validations. + * + * The keys must also be keys in the validations object, except `enable`, `disable`, + * and `default`. + */ +export type EnabledValidations = { + [key in keyof Omit<Validations, "enabled" | "disable"> | "default"]?: boolean; +}; +/** + * The configuration options for the rate limiter. + */ +export type Options = { + /** + * How long we should remember the requests. + * + * Defaults to `60000` ms (= 1 minute). + */ + windowMs: number; + /** + * The maximum number of connections to allow during the `window` before + * rate limiting the client. + * + * Can be the limit itself as a number or express middleware that parses + * the request and then figures out the limit. + * + * Defaults to `5`. + */ + limit: number | ValueDeterminingMiddleware<number>; + /** + * The response body to send back when a client is rate limited. + * + * Defaults to `'Too many requests, please try again later.'` + */ + message: any | ValueDeterminingMiddleware<any>; + /** + * The HTTP status code to send back when a client is rate limited. + * + * Defaults to `HTTP 429 Too Many Requests` (RFC 6585). + */ + statusCode: number; + /** + * Whether to send `X-RateLimit-*` headers with the rate limit and the number + * of requests. + * + * Defaults to `true` (for backward compatibility). + */ + legacyHeaders: boolean; + /** + * Whether to enable support for the standardized rate limit headers (`RateLimit-*`). + * + * Defaults to `false` (for backward compatibility, but its use is recommended). + */ + standardHeaders: boolean | DraftHeadersVersion; + /** + * The name used to identify the quota policy in the `RateLimit` headers as per + * the 8th draft of the IETF specification. + * + * Defaults to `{limit}-in-{window}`. + */ + identifier: string | ValueDeterminingMiddleware<string>; + /** + * The name of the property on the request object to store the rate limit info. + * + * Defaults to `rateLimit`. + */ + requestPropertyName: string; + /** + * If `true`, the library will (by default) skip all requests that have a 4XX + * or 5XX status. + * + * Defaults to `false`. + */ + skipFailedRequests: boolean; + /** + * If `true`, the library will (by default) skip all requests that have a + * status code less than 400. + * + * Defaults to `false`. + */ + skipSuccessfulRequests: boolean; + /** + * Method to generate custom identifiers for clients. + * + * By default, the client's IP address is used. + */ + keyGenerator: ValueDeterminingMiddleware<string>; + /** + * Express request handler that sends back a response when a client is + * rate-limited. + * + * By default, sends back the `statusCode` and `message` set via the options. + */ + handler: RateLimitExceededEventHandler; + /** + * Method (in the form of middleware) to determine whether or not this request + * counts towards a client's quota. + * + * By default, skips no requests. + */ + skip: ValueDeterminingMiddleware<boolean>; + /** + * Method to determine whether or not the request counts as 'succesful'. Used + * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true. + * + * By default, requests with a response status code less than 400 are considered + * successful. + */ + requestWasSuccessful: ValueDeterminingMiddleware<boolean>; + /** + * The `Store` to use to store the hit count for each client. + * + * By default, the built-in `MemoryStore` will be used. + */ + store: Store | LegacyStore; + /** + * The list of validation checks that should run. + */ + validate: boolean | EnabledValidations; + /** + * Whether to send `X-RateLimit-*` headers with the rate limit and the number + * of requests. + * + * @deprecated 6.x - This option was renamed to `legacyHeaders`. + */ + headers?: boolean; + /** + * The maximum number of connections to allow during the `window` before + * rate limiting the client. + * + * Can be the limit itself as a number or express middleware that parses + * the request and then figures out the limit. + * + * @deprecated 7.x - This option was renamed to `limit`. However, it will not + * be removed from the library in the foreseeable future. + */ + max?: number | ValueDeterminingMiddleware<number>; + /** + * If the Store generates an error, allow the request to pass. + */ + passOnStoreError: boolean; +}; +/** + * The extended request object that includes information about the client's + * rate limit. + */ +export type AugmentedRequest = Request & { + [key: string]: RateLimitInfo; +}; +/** + * The rate limit related information for each client included in the + * Express request object. + */ +export type RateLimitInfo = { + limit: number; + used: number; + remaining: number; + resetTime: Date | undefined; +}; +/** + * + * Create an instance of IP rate-limiting middleware for Express. + * + * @param passedOptions {Options} - Options to configure the rate limiter. + * + * @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration. + * + * @public + */ +export declare const rateLimit: (passedOptions?: Partial<Options>) => RateLimitRequestHandler; +/** + * The record that stores information about a client - namely, how many times + * they have hit the endpoint, and when their hit count resets. + * + * Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field. + */ +export type Client = { + totalHits: number; + resetTime: Date; +}; +/** + * A `Store` that stores the hit count for each client in memory. + * + * @public + */ +export declare class MemoryStore implements Store { + /** + * The duration of time before which all hit counts are reset (in milliseconds). + */ + windowMs: number; + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + previous: Map<string, Client>; + current: Map<string, Client>; + /** + * A reference to the active timer. + */ + interval?: NodeJS.Timeout; + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + localKeys: boolean; + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options: Options): void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + get(key: string): Promise<ClientRateLimitInfo | undefined>; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + increment(key: string): Promise<ClientRateLimitInfo>; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + decrement(key: string): Promise<void>; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + resetKey(key: string): Promise<void>; + /** + * Method to reset everyone's hit counter. + * + * @public + */ + resetAll(): Promise<void>; + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown(): void; + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + private resetClient; + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + private getClient; + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + private clearExpired; +} + +export { + rateLimit as default, +}; + +export {}; diff --git a/node_modules/express-rate-limit/dist/index.d.mts b/node_modules/express-rate-limit/dist/index.d.mts new file mode 100644 index 0000000..62fe123 --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.d.mts @@ -0,0 +1,584 @@ +// Generated by dts-bundle-generator v8.0.1 + +import { NextFunction, Request, RequestHandler, Response } from 'express'; + +declare const validations: { + enabled: { + [key: string]: boolean; + }; + disable(): void; + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip: string | undefined): void; + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request: Request): void; + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request: Request): void; + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits: any): void; + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store: Store): void; + /** + * Ensures a given key is incremented only once per request. + * + * @param request {Request} - The Express request object. + * @param store {Store} - The store class. + * @param key {string} - The key used to store the client's hit count. + * + * @returns {void} + */ + singleCount(request: Request, store: Store, key: string): void; + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit: number): void; + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers?: any): void; + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached?: any): void; + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version?: any): void; + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime?: Date): void; + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig(): void; + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store: Store): void; +}; +export type Validations = typeof validations; +declare const SUPPORTED_DRAFT_VERSIONS: string[]; +/** + * Callback that fires when a client's hit counter is incremented. + * + * @param error {Error | undefined} - The error that occurred, if any. + * @param totalHits {number} - The number of hits for that client so far. + * @param resetTime {Date | undefined} - The time when the counter resets. + */ +export type IncrementCallback = (error: Error | undefined, totalHits: number, resetTime: Date | undefined) => void; +/** + * Method (in the form of middleware) to generate/retrieve a value based on the + * incoming request. + * + * @param request {Request} - The Express request object. + * @param response {Response} - The Express response object. + * + * @returns {T} - The value needed. + */ +export type ValueDeterminingMiddleware<T> = (request: Request, response: Response) => T | Promise<T>; +/** + * Express request handler that sends back a response when a client is + * rate-limited. + * + * @param request {Request} - The Express request object. + * @param response {Response} - The Express response object. + * @param next {NextFunction} - The Express `next` function, can be called to skip responding. + * @param optionsUsed {Options} - The options used to set up the middleware. + */ +export type RateLimitExceededEventHandler = (request: Request, response: Response, next: NextFunction, optionsUsed: Options) => void; +/** + * Event callback that is triggered on a client's first request that exceeds the limit + * but not for subsequent requests. May be used for logging, etc. Should *not* + * send a response. + * + * @param request {Request} - The Express request object. + * @param response {Response} - The Express response object. + * @param optionsUsed {Options} - The options used to set up the middleware. + */ +export type RateLimitReachedEventHandler = (request: Request, response: Response, optionsUsed: Options) => void; +/** + * Data returned from the `Store` when a client's hit counter is incremented. + * + * @property totalHits {number} - The number of hits for that client so far. + * @property resetTime {Date | undefined} - The time when the counter resets. + */ +export type ClientRateLimitInfo = { + totalHits: number; + resetTime: Date | undefined; +}; +export type IncrementResponse = ClientRateLimitInfo; +/** + * A modified Express request handler with the rate limit functions. + */ +export type RateLimitRequestHandler = RequestHandler & { + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + getKey: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined; +}; +/** + * An interface that all hit counter stores must implement. + * + * @deprecated 6.x - Implement the `Store` interface instead. + */ +export type LegacyStore = { + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * @param callback {IncrementCallback} - The callback to call once the counter is incremented. + */ + incr: (key: string, callback: IncrementCallback) => void; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + decrement: (key: string) => void; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => void; + /** + * Method to reset everyone's hit counter. + */ + resetAll?: () => void; +}; +/** + * An interface that all hit counter stores must implement. + */ +export type Store = { + /** + * Method that initializes the store, and has access to the options passed to + * the middleware too. + * + * @param options {Options} - The options used to setup the middleware. + */ + init?: (options: Options) => void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client. + */ + increment: (key: string) => Promise<IncrementResponse> | IncrementResponse; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + decrement: (key: string) => Promise<void> | void; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => Promise<void> | void; + /** + * Method to reset everyone's hit counter. + */ + resetAll?: () => Promise<void> | void; + /** + * Method to shutdown the store, stop timers, and release all resources. + */ + shutdown?: () => Promise<void> | void; + /** + * Flag to indicate that keys incremented in one instance of this store can + * not affect other instances. Typically false if a database is used, true for + * MemoryStore. + * + * Used to help detect double-counting misconfigurations. + */ + localKeys?: boolean; + /** + * Optional value that the store prepends to keys + * + * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes + */ + prefix?: string; +}; +export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number]; +/** + * Validate configuration object for enabling or disabling specific validations. + * + * The keys must also be keys in the validations object, except `enable`, `disable`, + * and `default`. + */ +export type EnabledValidations = { + [key in keyof Omit<Validations, "enabled" | "disable"> | "default"]?: boolean; +}; +/** + * The configuration options for the rate limiter. + */ +export type Options = { + /** + * How long we should remember the requests. + * + * Defaults to `60000` ms (= 1 minute). + */ + windowMs: number; + /** + * The maximum number of connections to allow during the `window` before + * rate limiting the client. + * + * Can be the limit itself as a number or express middleware that parses + * the request and then figures out the limit. + * + * Defaults to `5`. + */ + limit: number | ValueDeterminingMiddleware<number>; + /** + * The response body to send back when a client is rate limited. + * + * Defaults to `'Too many requests, please try again later.'` + */ + message: any | ValueDeterminingMiddleware<any>; + /** + * The HTTP status code to send back when a client is rate limited. + * + * Defaults to `HTTP 429 Too Many Requests` (RFC 6585). + */ + statusCode: number; + /** + * Whether to send `X-RateLimit-*` headers with the rate limit and the number + * of requests. + * + * Defaults to `true` (for backward compatibility). + */ + legacyHeaders: boolean; + /** + * Whether to enable support for the standardized rate limit headers (`RateLimit-*`). + * + * Defaults to `false` (for backward compatibility, but its use is recommended). + */ + standardHeaders: boolean | DraftHeadersVersion; + /** + * The name used to identify the quota policy in the `RateLimit` headers as per + * the 8th draft of the IETF specification. + * + * Defaults to `{limit}-in-{window}`. + */ + identifier: string | ValueDeterminingMiddleware<string>; + /** + * The name of the property on the request object to store the rate limit info. + * + * Defaults to `rateLimit`. + */ + requestPropertyName: string; + /** + * If `true`, the library will (by default) skip all requests that have a 4XX + * or 5XX status. + * + * Defaults to `false`. + */ + skipFailedRequests: boolean; + /** + * If `true`, the library will (by default) skip all requests that have a + * status code less than 400. + * + * Defaults to `false`. + */ + skipSuccessfulRequests: boolean; + /** + * Method to generate custom identifiers for clients. + * + * By default, the client's IP address is used. + */ + keyGenerator: ValueDeterminingMiddleware<string>; + /** + * Express request handler that sends back a response when a client is + * rate-limited. + * + * By default, sends back the `statusCode` and `message` set via the options. + */ + handler: RateLimitExceededEventHandler; + /** + * Method (in the form of middleware) to determine whether or not this request + * counts towards a client's quota. + * + * By default, skips no requests. + */ + skip: ValueDeterminingMiddleware<boolean>; + /** + * Method to determine whether or not the request counts as 'succesful'. Used + * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true. + * + * By default, requests with a response status code less than 400 are considered + * successful. + */ + requestWasSuccessful: ValueDeterminingMiddleware<boolean>; + /** + * The `Store` to use to store the hit count for each client. + * + * By default, the built-in `MemoryStore` will be used. + */ + store: Store | LegacyStore; + /** + * The list of validation checks that should run. + */ + validate: boolean | EnabledValidations; + /** + * Whether to send `X-RateLimit-*` headers with the rate limit and the number + * of requests. + * + * @deprecated 6.x - This option was renamed to `legacyHeaders`. + */ + headers?: boolean; + /** + * The maximum number of connections to allow during the `window` before + * rate limiting the client. + * + * Can be the limit itself as a number or express middleware that parses + * the request and then figures out the limit. + * + * @deprecated 7.x - This option was renamed to `limit`. However, it will not + * be removed from the library in the foreseeable future. + */ + max?: number | ValueDeterminingMiddleware<number>; + /** + * If the Store generates an error, allow the request to pass. + */ + passOnStoreError: boolean; +}; +/** + * The extended request object that includes information about the client's + * rate limit. + */ +export type AugmentedRequest = Request & { + [key: string]: RateLimitInfo; +}; +/** + * The rate limit related information for each client included in the + * Express request object. + */ +export type RateLimitInfo = { + limit: number; + used: number; + remaining: number; + resetTime: Date | undefined; +}; +/** + * + * Create an instance of IP rate-limiting middleware for Express. + * + * @param passedOptions {Options} - Options to configure the rate limiter. + * + * @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration. + * + * @public + */ +export declare const rateLimit: (passedOptions?: Partial<Options>) => RateLimitRequestHandler; +/** + * The record that stores information about a client - namely, how many times + * they have hit the endpoint, and when their hit count resets. + * + * Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field. + */ +export type Client = { + totalHits: number; + resetTime: Date; +}; +/** + * A `Store` that stores the hit count for each client in memory. + * + * @public + */ +export declare class MemoryStore implements Store { + /** + * The duration of time before which all hit counts are reset (in milliseconds). + */ + windowMs: number; + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + previous: Map<string, Client>; + current: Map<string, Client>; + /** + * A reference to the active timer. + */ + interval?: NodeJS.Timeout; + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + localKeys: boolean; + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options: Options): void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + get(key: string): Promise<ClientRateLimitInfo | undefined>; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + increment(key: string): Promise<ClientRateLimitInfo>; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + decrement(key: string): Promise<void>; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + resetKey(key: string): Promise<void>; + /** + * Method to reset everyone's hit counter. + * + * @public + */ + resetAll(): Promise<void>; + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown(): void; + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + private resetClient; + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + private getClient; + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + private clearExpired; +} + +export { + rateLimit as default, +}; + +export {}; diff --git a/node_modules/express-rate-limit/dist/index.d.ts b/node_modules/express-rate-limit/dist/index.d.ts new file mode 100644 index 0000000..62fe123 --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.d.ts @@ -0,0 +1,584 @@ +// Generated by dts-bundle-generator v8.0.1 + +import { NextFunction, Request, RequestHandler, Response } from 'express'; + +declare const validations: { + enabled: { + [key: string]: boolean; + }; + disable(): void; + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip: string | undefined): void; + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request: Request): void; + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request: Request): void; + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits: any): void; + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store: Store): void; + /** + * Ensures a given key is incremented only once per request. + * + * @param request {Request} - The Express request object. + * @param store {Store} - The store class. + * @param key {string} - The key used to store the client's hit count. + * + * @returns {void} + */ + singleCount(request: Request, store: Store, key: string): void; + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit: number): void; + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers?: any): void; + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached?: any): void; + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version?: any): void; + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime?: Date): void; + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig(): void; + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store: Store): void; +}; +export type Validations = typeof validations; +declare const SUPPORTED_DRAFT_VERSIONS: string[]; +/** + * Callback that fires when a client's hit counter is incremented. + * + * @param error {Error | undefined} - The error that occurred, if any. + * @param totalHits {number} - The number of hits for that client so far. + * @param resetTime {Date | undefined} - The time when the counter resets. + */ +export type IncrementCallback = (error: Error | undefined, totalHits: number, resetTime: Date | undefined) => void; +/** + * Method (in the form of middleware) to generate/retrieve a value based on the + * incoming request. + * + * @param request {Request} - The Express request object. + * @param response {Response} - The Express response object. + * + * @returns {T} - The value needed. + */ +export type ValueDeterminingMiddleware<T> = (request: Request, response: Response) => T | Promise<T>; +/** + * Express request handler that sends back a response when a client is + * rate-limited. + * + * @param request {Request} - The Express request object. + * @param response {Response} - The Express response object. + * @param next {NextFunction} - The Express `next` function, can be called to skip responding. + * @param optionsUsed {Options} - The options used to set up the middleware. + */ +export type RateLimitExceededEventHandler = (request: Request, response: Response, next: NextFunction, optionsUsed: Options) => void; +/** + * Event callback that is triggered on a client's first request that exceeds the limit + * but not for subsequent requests. May be used for logging, etc. Should *not* + * send a response. + * + * @param request {Request} - The Express request object. + * @param response {Response} - The Express response object. + * @param optionsUsed {Options} - The options used to set up the middleware. + */ +export type RateLimitReachedEventHandler = (request: Request, response: Response, optionsUsed: Options) => void; +/** + * Data returned from the `Store` when a client's hit counter is incremented. + * + * @property totalHits {number} - The number of hits for that client so far. + * @property resetTime {Date | undefined} - The time when the counter resets. + */ +export type ClientRateLimitInfo = { + totalHits: number; + resetTime: Date | undefined; +}; +export type IncrementResponse = ClientRateLimitInfo; +/** + * A modified Express request handler with the rate limit functions. + */ +export type RateLimitRequestHandler = RequestHandler & { + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + getKey: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined; +}; +/** + * An interface that all hit counter stores must implement. + * + * @deprecated 6.x - Implement the `Store` interface instead. + */ +export type LegacyStore = { + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * @param callback {IncrementCallback} - The callback to call once the counter is incremented. + */ + incr: (key: string, callback: IncrementCallback) => void; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + decrement: (key: string) => void; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => void; + /** + * Method to reset everyone's hit counter. + */ + resetAll?: () => void; +}; +/** + * An interface that all hit counter stores must implement. + */ +export type Store = { + /** + * Method that initializes the store, and has access to the options passed to + * the middleware too. + * + * @param options {Options} - The options used to setup the middleware. + */ + init?: (options: Options) => void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client. + */ + increment: (key: string) => Promise<IncrementResponse> | IncrementResponse; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + decrement: (key: string) => Promise<void> | void; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => Promise<void> | void; + /** + * Method to reset everyone's hit counter. + */ + resetAll?: () => Promise<void> | void; + /** + * Method to shutdown the store, stop timers, and release all resources. + */ + shutdown?: () => Promise<void> | void; + /** + * Flag to indicate that keys incremented in one instance of this store can + * not affect other instances. Typically false if a database is used, true for + * MemoryStore. + * + * Used to help detect double-counting misconfigurations. + */ + localKeys?: boolean; + /** + * Optional value that the store prepends to keys + * + * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes + */ + prefix?: string; +}; +export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number]; +/** + * Validate configuration object for enabling or disabling specific validations. + * + * The keys must also be keys in the validations object, except `enable`, `disable`, + * and `default`. + */ +export type EnabledValidations = { + [key in keyof Omit<Validations, "enabled" | "disable"> | "default"]?: boolean; +}; +/** + * The configuration options for the rate limiter. + */ +export type Options = { + /** + * How long we should remember the requests. + * + * Defaults to `60000` ms (= 1 minute). + */ + windowMs: number; + /** + * The maximum number of connections to allow during the `window` before + * rate limiting the client. + * + * Can be the limit itself as a number or express middleware that parses + * the request and then figures out the limit. + * + * Defaults to `5`. + */ + limit: number | ValueDeterminingMiddleware<number>; + /** + * The response body to send back when a client is rate limited. + * + * Defaults to `'Too many requests, please try again later.'` + */ + message: any | ValueDeterminingMiddleware<any>; + /** + * The HTTP status code to send back when a client is rate limited. + * + * Defaults to `HTTP 429 Too Many Requests` (RFC 6585). + */ + statusCode: number; + /** + * Whether to send `X-RateLimit-*` headers with the rate limit and the number + * of requests. + * + * Defaults to `true` (for backward compatibility). + */ + legacyHeaders: boolean; + /** + * Whether to enable support for the standardized rate limit headers (`RateLimit-*`). + * + * Defaults to `false` (for backward compatibility, but its use is recommended). + */ + standardHeaders: boolean | DraftHeadersVersion; + /** + * The name used to identify the quota policy in the `RateLimit` headers as per + * the 8th draft of the IETF specification. + * + * Defaults to `{limit}-in-{window}`. + */ + identifier: string | ValueDeterminingMiddleware<string>; + /** + * The name of the property on the request object to store the rate limit info. + * + * Defaults to `rateLimit`. + */ + requestPropertyName: string; + /** + * If `true`, the library will (by default) skip all requests that have a 4XX + * or 5XX status. + * + * Defaults to `false`. + */ + skipFailedRequests: boolean; + /** + * If `true`, the library will (by default) skip all requests that have a + * status code less than 400. + * + * Defaults to `false`. + */ + skipSuccessfulRequests: boolean; + /** + * Method to generate custom identifiers for clients. + * + * By default, the client's IP address is used. + */ + keyGenerator: ValueDeterminingMiddleware<string>; + /** + * Express request handler that sends back a response when a client is + * rate-limited. + * + * By default, sends back the `statusCode` and `message` set via the options. + */ + handler: RateLimitExceededEventHandler; + /** + * Method (in the form of middleware) to determine whether or not this request + * counts towards a client's quota. + * + * By default, skips no requests. + */ + skip: ValueDeterminingMiddleware<boolean>; + /** + * Method to determine whether or not the request counts as 'succesful'. Used + * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true. + * + * By default, requests with a response status code less than 400 are considered + * successful. + */ + requestWasSuccessful: ValueDeterminingMiddleware<boolean>; + /** + * The `Store` to use to store the hit count for each client. + * + * By default, the built-in `MemoryStore` will be used. + */ + store: Store | LegacyStore; + /** + * The list of validation checks that should run. + */ + validate: boolean | EnabledValidations; + /** + * Whether to send `X-RateLimit-*` headers with the rate limit and the number + * of requests. + * + * @deprecated 6.x - This option was renamed to `legacyHeaders`. + */ + headers?: boolean; + /** + * The maximum number of connections to allow during the `window` before + * rate limiting the client. + * + * Can be the limit itself as a number or express middleware that parses + * the request and then figures out the limit. + * + * @deprecated 7.x - This option was renamed to `limit`. However, it will not + * be removed from the library in the foreseeable future. + */ + max?: number | ValueDeterminingMiddleware<number>; + /** + * If the Store generates an error, allow the request to pass. + */ + passOnStoreError: boolean; +}; +/** + * The extended request object that includes information about the client's + * rate limit. + */ +export type AugmentedRequest = Request & { + [key: string]: RateLimitInfo; +}; +/** + * The rate limit related information for each client included in the + * Express request object. + */ +export type RateLimitInfo = { + limit: number; + used: number; + remaining: number; + resetTime: Date | undefined; +}; +/** + * + * Create an instance of IP rate-limiting middleware for Express. + * + * @param passedOptions {Options} - Options to configure the rate limiter. + * + * @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration. + * + * @public + */ +export declare const rateLimit: (passedOptions?: Partial<Options>) => RateLimitRequestHandler; +/** + * The record that stores information about a client - namely, how many times + * they have hit the endpoint, and when their hit count resets. + * + * Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field. + */ +export type Client = { + totalHits: number; + resetTime: Date; +}; +/** + * A `Store` that stores the hit count for each client in memory. + * + * @public + */ +export declare class MemoryStore implements Store { + /** + * The duration of time before which all hit counts are reset (in milliseconds). + */ + windowMs: number; + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + previous: Map<string, Client>; + current: Map<string, Client>; + /** + * A reference to the active timer. + */ + interval?: NodeJS.Timeout; + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + localKeys: boolean; + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options: Options): void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + get(key: string): Promise<ClientRateLimitInfo | undefined>; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + increment(key: string): Promise<ClientRateLimitInfo>; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + decrement(key: string): Promise<void>; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + resetKey(key: string): Promise<void>; + /** + * Method to reset everyone's hit counter. + * + * @public + */ + resetAll(): Promise<void>; + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown(): void; + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + private resetClient; + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + private getClient; + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + private clearExpired; +} + +export { + rateLimit as default, +}; + +export {}; diff --git a/node_modules/express-rate-limit/dist/index.mjs b/node_modules/express-rate-limit/dist/index.mjs new file mode 100644 index 0000000..f8b70ce --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.mjs @@ -0,0 +1,809 @@ +// source/headers.ts +import { Buffer } from "buffer"; +import { createHash } from "crypto"; +var SUPPORTED_DRAFT_VERSIONS = ["draft-6", "draft-7", "draft-8"]; +var getResetSeconds = (resetTime, windowMs) => { + let resetSeconds = void 0; + if (resetTime) { + const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3); + resetSeconds = Math.max(0, deltaSeconds); + } else if (windowMs) { + resetSeconds = Math.ceil(windowMs / 1e3); + } + return resetSeconds; +}; +var getPartitionKey = (key) => { + const hash = createHash("sha256"); + hash.update(key); + const partitionKey = hash.digest("hex").slice(0, 12); + return Buffer.from(partitionKey).toString("base64"); +}; +var setLegacyHeaders = (response, info) => { + if (response.headersSent) + return; + response.setHeader("X-RateLimit-Limit", info.limit.toString()); + response.setHeader("X-RateLimit-Remaining", info.remaining.toString()); + if (info.resetTime instanceof Date) { + response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString()); + response.setHeader( + "X-RateLimit-Reset", + Math.ceil(info.resetTime.getTime() / 1e3).toString() + ); + } +}; +var setDraft6Headers = (response, info, windowMs) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime); + response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`); + response.setHeader("RateLimit-Limit", info.limit.toString()); + response.setHeader("RateLimit-Remaining", info.remaining.toString()); + if (resetSeconds) + response.setHeader("RateLimit-Reset", resetSeconds.toString()); +}; +var setDraft7Headers = (response, info, windowMs) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`); + response.setHeader( + "RateLimit", + `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}` + ); +}; +var setDraft8Headers = (response, info, windowMs, name, key) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + const partitionKey = getPartitionKey(key); + const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`; + const header = `r=${info.remaining}; t=${resetSeconds}`; + response.append("RateLimit-Policy", `"${name}"; ${policy}`); + response.append("RateLimit", `"${name}"; ${header}`); +}; +var setRetryAfterHeader = (response, info, windowMs) => { + if (response.headersSent) + return; + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + response.setHeader("Retry-After", resetSeconds.toString()); +}; + +// source/validations.ts +import { isIP } from "net"; +var ValidationError = class extends Error { + /** + * The code must be a string, in snake case and all capital, that starts with + * the substring `ERR_ERL_`. + * + * The message must be a string, starting with an uppercase character, + * describing the issue in detail. + */ + constructor(code, message) { + const url = `https://express-rate-limit.github.io/${code}/`; + super(`${message} See ${url} for more information.`); + this.name = this.constructor.name; + this.code = code; + this.help = url; + } +}; +var ChangeWarning = class extends ValidationError { +}; +var usedStores = /* @__PURE__ */ new Set(); +var singleCountKeys = /* @__PURE__ */ new WeakMap(); +var validations = { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + enabled: { + default: true + }, + // Should be EnabledValidations type, but that's a circular reference + disable() { + for (const k of Object.keys(this.enabled)) + this.enabled[k] = false; + }, + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip) { + if (ip === void 0) { + throw new ValidationError( + "ERR_ERL_UNDEFINED_IP_ADDRESS", + `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.` + ); + } + if (!isIP(ip)) { + throw new ValidationError( + "ERR_ERL_INVALID_IP_ADDRESS", + `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.` + ); + } + }, + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request) { + if (request.app.get("trust proxy") === true) { + throw new ValidationError( + "ERR_ERL_PERMISSIVE_TRUST_PROXY", + `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.` + ); + } + }, + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request) { + if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) { + throw new ValidationError( + "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR", + `The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.` + ); + } + }, + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits) { + if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) { + throw new ValidationError( + "ERR_ERL_INVALID_HITS", + `The totalHits value returned from the store must be a positive integer, got ${hits}` + ); + } + }, + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store) { + if (usedStores.has(store)) { + const maybeUniquePrefix = store?.localKeys ? "" : " (with a unique prefix)"; + throw new ValidationError( + "ERR_ERL_STORE_REUSE", + `A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.` + ); + } + usedStores.add(store); + }, + /** + * Ensures a given key is incremented only once per request. + * + * @param request {Request} - The Express request object. + * @param store {Store} - The store class. + * @param key {string} - The key used to store the client's hit count. + * + * @returns {void} + */ + singleCount(request, store, key) { + let storeKeys = singleCountKeys.get(request); + if (!storeKeys) { + storeKeys = /* @__PURE__ */ new Map(); + singleCountKeys.set(request, storeKeys); + } + const storeKey = store.localKeys ? store : store.constructor.name; + let keys = storeKeys.get(storeKey); + if (!keys) { + keys = []; + storeKeys.set(storeKey, keys); + } + const prefixedKey = `${store.prefix ?? ""}${key}`; + if (keys.includes(prefixedKey)) { + throw new ValidationError( + "ERR_ERL_DOUBLE_COUNT", + `The hit count for ${key} was incremented more than once for a single request.` + ); + } + keys.push(prefixedKey); + }, + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit) { + if (limit === 0) { + throw new ChangeWarning( + "WRN_ERL_MAX_ZERO", + `Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7` + ); + } + }, + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers) { + if (draft_polli_ratelimit_headers) { + throw new ChangeWarning( + "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS", + `The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.` + ); + } + }, + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached) { + if (onLimitReached) { + throw new ChangeWarning( + "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED", + `The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.` + ); + } + }, + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version) { + if (typeof version !== "string" || !SUPPORTED_DRAFT_VERSIONS.includes(version)) { + const versionString = SUPPORTED_DRAFT_VERSIONS.join(", "); + throw new ValidationError( + "ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION", + `standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.` + ); + } + }, + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime) { + if (!resetTime) { + throw new ValidationError( + "ERR_ERL_HEADERS_NO_RESET", + `standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.` + ); + } + }, + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig() { + const supportedValidations = Object.keys(this).filter( + (k) => !["enabled", "disable"].includes(k) + ); + supportedValidations.push("default"); + for (const key of Object.keys(this.enabled)) { + if (!supportedValidations.includes(key)) { + throw new ValidationError( + "ERR_ERL_UNKNOWN_VALIDATION", + `options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join( + ", " + )}.` + ); + } + } + }, + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store) { + const { stack } = new Error( + "express-rate-limit validation check (set options.validate.creationStack=false to disable)" + ); + if (stack?.includes("Layer.handle [as handle_request]")) { + if (!store.localKeys) { + throw new ValidationError( + "ERR_ERL_CREATED_IN_REQUEST_HANDLER", + "express-rate-limit instance should *usually* be created at app initialization, not when responding to a request." + ); + } + throw new ValidationError( + "ERR_ERL_CREATED_IN_REQUEST_HANDLER", + `express-rate-limit instance should be created at app initialization, not when responding to a request.` + ); + } + } +}; +var getValidations = (_enabled) => { + let enabled; + if (typeof _enabled === "boolean") { + enabled = { + default: _enabled + }; + } else { + enabled = { + default: true, + ..._enabled + }; + } + const wrappedValidations = { + enabled + }; + for (const [name, validation] of Object.entries(validations)) { + if (typeof validation === "function") + wrappedValidations[name] = (...args) => { + if (!(enabled[name] ?? enabled.default)) { + return; + } + try { + ; + validation.apply( + wrappedValidations, + args + ); + } catch (error) { + if (error instanceof ChangeWarning) + console.warn(error); + else + console.error(error); + } + }; + } + return wrappedValidations; +}; + +// source/memory-store.ts +var MemoryStore = class { + constructor() { + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + this.previous = /* @__PURE__ */ new Map(); + this.current = /* @__PURE__ */ new Map(); + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + this.localKeys = true; + } + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options) { + this.windowMs = options.windowMs; + if (this.interval) + clearInterval(this.interval); + this.interval = setInterval(() => { + this.clearExpired(); + }, this.windowMs); + if (this.interval.unref) + this.interval.unref(); + } + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + async get(key) { + return this.current.get(key) ?? this.previous.get(key); + } + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + async increment(key) { + const client = this.getClient(key); + const now = Date.now(); + if (client.resetTime.getTime() <= now) { + this.resetClient(client, now); + } + client.totalHits++; + return client; + } + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + async decrement(key) { + const client = this.getClient(key); + if (client.totalHits > 0) + client.totalHits--; + } + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + async resetKey(key) { + this.current.delete(key); + this.previous.delete(key); + } + /** + * Method to reset everyone's hit counter. + * + * @public + */ + async resetAll() { + this.current.clear(); + this.previous.clear(); + } + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown() { + clearInterval(this.interval); + void this.resetAll(); + } + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + resetClient(client, now = Date.now()) { + client.totalHits = 0; + client.resetTime.setTime(now + this.windowMs); + return client; + } + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + getClient(key) { + if (this.current.has(key)) + return this.current.get(key); + let client; + if (this.previous.has(key)) { + client = this.previous.get(key); + this.previous.delete(key); + } else { + client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() }; + this.resetClient(client); + } + this.current.set(key, client); + return client; + } + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + clearExpired() { + this.previous = this.current; + this.current = /* @__PURE__ */ new Map(); + } +}; + +// source/lib.ts +var isLegacyStore = (store) => ( + // Check that `incr` exists but `increment` does not - store authors might want + // to keep both around for backwards compatibility. + typeof store.incr === "function" && typeof store.increment !== "function" +); +var promisifyStore = (passedStore) => { + if (!isLegacyStore(passedStore)) { + return passedStore; + } + const legacyStore = passedStore; + class PromisifiedStore { + async increment(key) { + return new Promise((resolve, reject) => { + legacyStore.incr( + key, + (error, totalHits, resetTime) => { + if (error) + reject(error); + resolve({ totalHits, resetTime }); + } + ); + }); + } + async decrement(key) { + return legacyStore.decrement(key); + } + async resetKey(key) { + return legacyStore.resetKey(key); + } + /* istanbul ignore next */ + async resetAll() { + if (typeof legacyStore.resetAll === "function") + return legacyStore.resetAll(); + } + } + return new PromisifiedStore(); +}; +var getOptionsFromConfig = (config) => { + const { validations: validations2, ...directlyPassableEntries } = config; + return { + ...directlyPassableEntries, + validate: validations2.enabled + }; +}; +var omitUndefinedOptions = (passedOptions) => { + const omittedOptions = {}; + for (const k of Object.keys(passedOptions)) { + const key = k; + if (passedOptions[key] !== void 0) { + omittedOptions[key] = passedOptions[key]; + } + } + return omittedOptions; +}; +var parseOptions = (passedOptions) => { + const notUndefinedOptions = omitUndefinedOptions(passedOptions); + const validations2 = getValidations(notUndefinedOptions?.validate ?? true); + validations2.validationsConfig(); + validations2.draftPolliHeaders( + // @ts-expect-error see the note above. + notUndefinedOptions.draft_polli_ratelimit_headers + ); + validations2.onLimitReached(notUndefinedOptions.onLimitReached); + let standardHeaders = notUndefinedOptions.standardHeaders ?? false; + if (standardHeaders === true) + standardHeaders = "draft-6"; + const config = { + windowMs: 60 * 1e3, + limit: passedOptions.max ?? 5, + // `max` is deprecated, but support it anyways. + message: "Too many requests, please try again later.", + statusCode: 429, + legacyHeaders: passedOptions.headers ?? true, + identifier(request, _response) { + let duration = ""; + const property = config.requestPropertyName; + const { limit } = request[property]; + const seconds = config.windowMs / 1e3; + const minutes = config.windowMs / (1e3 * 60); + const hours = config.windowMs / (1e3 * 60 * 60); + const days = config.windowMs / (1e3 * 60 * 60 * 24); + if (seconds < 60) + duration = `${seconds}sec`; + else if (minutes < 60) + duration = `${minutes}min`; + else if (hours < 24) + duration = `${hours}hr${hours > 1 ? "s" : ""}`; + else + duration = `${days}day${days > 1 ? "s" : ""}`; + return `${limit}-in-${duration}`; + }, + requestPropertyName: "rateLimit", + skipFailedRequests: false, + skipSuccessfulRequests: false, + requestWasSuccessful: (_request, response) => response.statusCode < 400, + skip: (_request, _response) => false, + keyGenerator(request, _response) { + validations2.ip(request.ip); + validations2.trustProxy(request); + validations2.xForwardedForHeader(request); + return request.ip; + }, + async handler(request, response, _next, _optionsUsed) { + response.status(config.statusCode); + const message = typeof config.message === "function" ? await config.message( + request, + response + ) : config.message; + if (!response.writableEnded) { + response.send(message); + } + }, + passOnStoreError: false, + // Allow the default options to be overriden by the passed options. + ...notUndefinedOptions, + // `standardHeaders` is resolved into a draft version above, use that. + standardHeaders, + // Note that this field is declared after the user's options are spread in, + // so that this field doesn't get overriden with an un-promisified store! + store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()), + // Print an error to the console if a few known misconfigurations are detected. + validations: validations2 + }; + if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || config.store.resetAll !== void 0 && typeof config.store.resetAll !== "function" || config.store.init !== void 0 && typeof config.store.init !== "function") { + throw new TypeError( + "An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface." + ); + } + return config; +}; +var handleAsyncErrors = (fn) => async (request, response, next) => { + try { + await Promise.resolve(fn(request, response, next)).catch(next); + } catch (error) { + next(error); + } +}; +var rateLimit = (passedOptions) => { + const config = parseOptions(passedOptions ?? {}); + const options = getOptionsFromConfig(config); + config.validations.creationStack(config.store); + config.validations.unsharedStore(config.store); + if (typeof config.store.init === "function") + config.store.init(options); + const middleware = handleAsyncErrors( + async (request, response, next) => { + const skip = await config.skip(request, response); + if (skip) { + next(); + return; + } + const augmentedRequest = request; + const key = await config.keyGenerator(request, response); + let totalHits = 0; + let resetTime; + try { + const incrementResult = await config.store.increment(key); + totalHits = incrementResult.totalHits; + resetTime = incrementResult.resetTime; + } catch (error) { + if (config.passOnStoreError) { + console.error( + "express-rate-limit: error from store, allowing request without rate-limiting.", + error + ); + next(); + return; + } + throw error; + } + config.validations.positiveHits(totalHits); + config.validations.singleCount(request, config.store, key); + const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit; + const limit = await retrieveLimit; + config.validations.limit(limit); + const info = { + limit, + used: totalHits, + remaining: Math.max(limit - totalHits, 0), + resetTime + }; + Object.defineProperty(info, "current", { + configurable: false, + enumerable: false, + value: totalHits + }); + augmentedRequest[config.requestPropertyName] = info; + if (config.legacyHeaders && !response.headersSent) { + setLegacyHeaders(response, info); + } + if (config.standardHeaders && !response.headersSent) { + switch (config.standardHeaders) { + case "draft-6": { + setDraft6Headers(response, info, config.windowMs); + break; + } + case "draft-7": { + config.validations.headersResetTime(info.resetTime); + setDraft7Headers(response, info, config.windowMs); + break; + } + case "draft-8": { + const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier; + const name = await retrieveName; + config.validations.headersResetTime(info.resetTime); + setDraft8Headers(response, info, config.windowMs, name, key); + break; + } + default: { + config.validations.headersDraftVersion(config.standardHeaders); + break; + } + } + } + if (config.skipFailedRequests || config.skipSuccessfulRequests) { + let decremented = false; + const decrementKey = async () => { + if (!decremented) { + await config.store.decrement(key); + decremented = true; + } + }; + if (config.skipFailedRequests) { + response.on("finish", async () => { + if (!await config.requestWasSuccessful(request, response)) + await decrementKey(); + }); + response.on("close", async () => { + if (!response.writableEnded) + await decrementKey(); + }); + response.on("error", async () => { + await decrementKey(); + }); + } + if (config.skipSuccessfulRequests) { + response.on("finish", async () => { + if (await config.requestWasSuccessful(request, response)) + await decrementKey(); + }); + } + } + config.validations.disable(); + if (totalHits > limit) { + if (config.legacyHeaders || config.standardHeaders) { + setRetryAfterHeader(response, info, config.windowMs); + } + config.handler(request, response, next, options); + return; + } + next(); + } + ); + const getThrowFn = () => { + throw new Error("The current store does not support the get/getKey method"); + }; + middleware.resetKey = config.store.resetKey.bind(config.store); + middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn; + return middleware; +}; +var lib_default = rateLimit; +export { + MemoryStore, + lib_default as default, + lib_default as rateLimit +}; diff --git a/node_modules/express-rate-limit/package.json b/node_modules/express-rate-limit/package.json new file mode 100644 index 0000000..1783725 --- /dev/null +++ b/node_modules/express-rate-limit/package.json @@ -0,0 +1,133 @@ +{ + "name": "express-rate-limit", + "version": "7.5.0", + "description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.", + "author": { + "name": "Nathan Friedly", + "url": "http://nfriedly.com/" + }, + "license": "MIT", + "homepage": "https://github.com/express-rate-limit/express-rate-limit", + "repository": { + "type": "git", + "url": "git+https://github.com/express-rate-limit/express-rate-limit.git" + }, + "funding": "https://github.com/sponsors/express-rate-limit", + "keywords": [ + "express-rate-limit", + "express", + "rate", + "limit", + "ratelimit", + "rate-limit", + "middleware", + "ip", + "auth", + "authorization", + "security", + "brute", + "force", + "bruteforce", + "brute-force", + "attack" + ], + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist/", + "tsconfig.json" + ], + "engines": { + "node": ">= 16" + }, + "scripts": { + "clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz", + "build:cjs": "esbuild --platform=node --bundle --target=es2022 --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;\" source/index.ts", + "build:esm": "esbuild --platform=node --bundle --target=es2022 --format=esm --outfile=dist/index.mjs source/index.ts", + "build:types": "dts-bundle-generator --out-file=dist/index.d.ts source/index.ts && cp dist/index.d.ts dist/index.d.cts && cp dist/index.d.ts dist/index.d.mts", + "compile": "run-s clean build:*", + "docs": "cd docs && mintlify dev", + "lint:code": "xo", + "lint:rest": "prettier --check .", + "lint": "run-s lint:*", + "format:code": "xo --fix", + "format:rest": "prettier --write .", + "format": "run-s format:*", + "test:lib": "jest", + "test:ext": "cd test/external/ && bash run-all-tests", + "test": "run-s lint test:lib", + "pre-commit": "lint-staged", + "prepare": "run-s compile && husky install config/husky" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + }, + "devDependencies": { + "@express-rate-limit/prettier": "1.1.1", + "@express-rate-limit/tsconfig": "1.0.2", + "@jest/globals": "29.7.0", + "@types/express": "4.17.20", + "@types/jest": "29.5.6", + "@types/node": "20.8.7", + "@types/supertest": "2.0.15", + "del-cli": "5.1.0", + "dts-bundle-generator": "8.0.1", + "esbuild": "0.19.5", + "express": "4.21.1", + "husky": "8.0.3", + "jest": "29.7.0", + "lint-staged": "15.0.2", + "mintlify": "4.0.63", + "npm-run-all": "4.1.5", + "ratelimit-header-parser": "0.1.0", + "supertest": "6.3.3", + "ts-jest": "29.1.1", + "ts-node": "10.9.1", + "typescript": "5.2.2", + "xo": "0.56.0" + }, + "xo": { + "prettier": true, + "rules": { + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-dynamic-delete": 0, + "@typescript-eslint/no-confusing-void-expression": 0, + "@typescript-eslint/consistent-indexed-object-style": [ + "error", + "index-signature" + ], + "n/no-unsupported-features/es-syntax": 0 + }, + "overrides": [ + { + "files": "test/library/*.ts", + "rules": { + "@typescript-eslint/no-unsafe-argument": 0, + "@typescript-eslint/no-unsafe-assignment": 0 + } + } + ], + "ignore": [ + "test/external" + ] + }, + "prettier": "@express-rate-limit/prettier", + "lint-staged": { + "{source,test}/**/*.ts": "xo --fix", + "**/*.{json,yaml,md}": "prettier --write " + } +} diff --git a/node_modules/express-rate-limit/tsconfig.json b/node_modules/express-rate-limit/tsconfig.json new file mode 100644 index 0000000..52b6ff1 --- /dev/null +++ b/node_modules/express-rate-limit/tsconfig.json @@ -0,0 +1,8 @@ +{ + "include": ["source/"], + "exclude": ["node_modules/"], + "extends": "@express-rate-limit/tsconfig", + "compilerOptions": { + "target": "ES2020" + } +} diff --git a/node_modules/express-session/index.js b/node_modules/express-session/index.js new file mode 100644 index 0000000..d41b237 --- /dev/null +++ b/node_modules/express-session/index.js @@ -0,0 +1,693 @@ +/*! + * express-session + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var Buffer = require('safe-buffer').Buffer +var cookie = require('cookie'); +var crypto = require('crypto') +var debug = require('debug')('express-session'); +var deprecate = require('depd')('express-session'); +var onHeaders = require('on-headers') +var parseUrl = require('parseurl'); +var signature = require('cookie-signature') +var uid = require('uid-safe').sync + +var Cookie = require('./session/cookie') +var MemoryStore = require('./session/memory') +var Session = require('./session/session') +var Store = require('./session/store') + +// environment + +var env = process.env.NODE_ENV; + +/** + * Expose the middleware. + */ + +exports = module.exports = session; + +/** + * Expose constructors. + */ + +exports.Store = Store; +exports.Cookie = Cookie; +exports.Session = Session; +exports.MemoryStore = MemoryStore; + +/** + * Warning message for `MemoryStore` usage in production. + * @private + */ + +var warning = 'Warning: connect.session() MemoryStore is not\n' + + 'designed for a production environment, as it will leak\n' + + 'memory, and will not scale past a single process.'; + +/** + * Node.js 0.8+ async implementation. + * @private + */ + +/* istanbul ignore next */ +var defer = typeof setImmediate === 'function' + ? setImmediate + : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } + +/** + * Setup session store with the given `options`. + * + * @param {Object} [options] + * @param {Object} [options.cookie] Options for cookie + * @param {Function} [options.genid] + * @param {String} [options.name=connect.sid] Session ID cookie name + * @param {Boolean} [options.proxy] + * @param {Boolean} [options.resave] Resave unmodified sessions back to the store + * @param {Boolean} [options.rolling] Enable/disable rolling session expiration + * @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store + * @param {String|Array} [options.secret] Secret for signing session ID + * @param {Object} [options.store=MemoryStore] Session store + * @param {String} [options.unset] + * @return {Function} middleware + * @public + */ + +function session(options) { + var opts = options || {} + + // get the cookie options + var cookieOptions = opts.cookie || {} + + // get the session id generate function + var generateId = opts.genid || generateSessionId + + // get the session cookie name + var name = opts.name || opts.key || 'connect.sid' + + // get the session store + var store = opts.store || new MemoryStore() + + // get the trust proxy setting + var trustProxy = opts.proxy + + // get the resave session option + var resaveSession = opts.resave; + + // get the rolling session option + var rollingSessions = Boolean(opts.rolling) + + // get the save uninitialized session option + var saveUninitializedSession = opts.saveUninitialized + + // get the cookie signing secret + var secret = opts.secret + + if (typeof generateId !== 'function') { + throw new TypeError('genid option must be a function'); + } + + if (resaveSession === undefined) { + deprecate('undefined resave option; provide resave option'); + resaveSession = true; + } + + if (saveUninitializedSession === undefined) { + deprecate('undefined saveUninitialized option; provide saveUninitialized option'); + saveUninitializedSession = true; + } + + if (opts.unset && opts.unset !== 'destroy' && opts.unset !== 'keep') { + throw new TypeError('unset option must be "destroy" or "keep"'); + } + + // TODO: switch to "destroy" on next major + var unsetDestroy = opts.unset === 'destroy' + + if (Array.isArray(secret) && secret.length === 0) { + throw new TypeError('secret option array must contain one or more strings'); + } + + if (secret && !Array.isArray(secret)) { + secret = [secret]; + } + + if (!secret) { + deprecate('req.secret; provide secret option'); + } + + // notify user that this store is not + // meant for a production environment + /* istanbul ignore next: not tested */ + if (env === 'production' && store instanceof MemoryStore) { + console.warn(warning); + } + + // generates the new session + store.generate = function(req){ + req.sessionID = generateId(req); + req.session = new Session(req); + req.session.cookie = new Cookie(cookieOptions); + + if (cookieOptions.secure === 'auto') { + req.session.cookie.secure = issecure(req, trustProxy); + } + }; + + var storeImplementsTouch = typeof store.touch === 'function'; + + // register event listeners for the store to track readiness + var storeReady = true + store.on('disconnect', function ondisconnect() { + storeReady = false + }) + store.on('connect', function onconnect() { + storeReady = true + }) + + return function session(req, res, next) { + // self-awareness + if (req.session) { + next() + return + } + + // Handle connection as if there is no session if + // the store has temporarily disconnected etc + if (!storeReady) { + debug('store is disconnected') + next() + return + } + + // pathname mismatch + var originalPath = parseUrl.original(req).pathname || '/' + if (originalPath.indexOf(cookieOptions.path || '/') !== 0) { + debug('pathname mismatch') + next() + return + } + + // ensure a secret is available or bail + if (!secret && !req.secret) { + next(new Error('secret option required for sessions')); + return; + } + + // backwards compatibility for signed cookies + // req.secret is passed from the cookie parser middleware + var secrets = secret || [req.secret]; + + var originalHash; + var originalId; + var savedHash; + var touched = false + + // expose store + req.sessionStore = store; + + // get the session ID from the cookie + var cookieId = req.sessionID = getcookie(req, name, secrets); + + // set-cookie + onHeaders(res, function(){ + if (!req.session) { + debug('no session'); + return; + } + + if (!shouldSetCookie(req)) { + return; + } + + // only send secure cookies via https + if (req.session.cookie.secure && !issecure(req, trustProxy)) { + debug('not secured'); + return; + } + + if (!touched) { + // touch session + req.session.touch() + touched = true + } + + // set cookie + try { + setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data) + } catch (err) { + defer(next, err) + } + }); + + // proxy end() to commit the session + var _end = res.end; + var _write = res.write; + var ended = false; + res.end = function end(chunk, encoding) { + if (ended) { + return false; + } + + ended = true; + + var ret; + var sync = true; + + function writeend() { + if (sync) { + ret = _end.call(res, chunk, encoding); + sync = false; + return; + } + + _end.call(res); + } + + function writetop() { + if (!sync) { + return ret; + } + + if (!res._header) { + res._implicitHeader() + } + + if (chunk == null) { + ret = true; + return ret; + } + + var contentLength = Number(res.getHeader('Content-Length')); + + if (!isNaN(contentLength) && contentLength > 0) { + // measure chunk + chunk = !Buffer.isBuffer(chunk) + ? Buffer.from(chunk, encoding) + : chunk; + encoding = undefined; + + if (chunk.length !== 0) { + debug('split response'); + ret = _write.call(res, chunk.slice(0, chunk.length - 1)); + chunk = chunk.slice(chunk.length - 1, chunk.length); + return ret; + } + } + + ret = _write.call(res, chunk, encoding); + sync = false; + + return ret; + } + + if (shouldDestroy(req)) { + // destroy session + debug('destroying'); + store.destroy(req.sessionID, function ondestroy(err) { + if (err) { + defer(next, err); + } + + debug('destroyed'); + writeend(); + }); + + return writetop(); + } + + // no session to save + if (!req.session) { + debug('no session'); + return _end.call(res, chunk, encoding); + } + + if (!touched) { + // touch session + req.session.touch() + touched = true + } + + if (shouldSave(req)) { + req.session.save(function onsave(err) { + if (err) { + defer(next, err); + } + + writeend(); + }); + + return writetop(); + } else if (storeImplementsTouch && shouldTouch(req)) { + // store implements touch method + debug('touching'); + store.touch(req.sessionID, req.session, function ontouch(err) { + if (err) { + defer(next, err); + } + + debug('touched'); + writeend(); + }); + + return writetop(); + } + + return _end.call(res, chunk, encoding); + }; + + // generate the session + function generate() { + store.generate(req); + originalId = req.sessionID; + originalHash = hash(req.session); + wrapmethods(req.session); + } + + // inflate the session + function inflate (req, sess) { + store.createSession(req, sess) + originalId = req.sessionID + originalHash = hash(sess) + + if (!resaveSession) { + savedHash = originalHash + } + + wrapmethods(req.session) + } + + function rewrapmethods (sess, callback) { + return function () { + if (req.session !== sess) { + wrapmethods(req.session) + } + + callback.apply(this, arguments) + } + } + + // wrap session methods + function wrapmethods(sess) { + var _reload = sess.reload + var _save = sess.save; + + function reload(callback) { + debug('reloading %s', this.id) + _reload.call(this, rewrapmethods(this, callback)) + } + + function save() { + debug('saving %s', this.id); + savedHash = hash(this); + _save.apply(this, arguments); + } + + Object.defineProperty(sess, 'reload', { + configurable: true, + enumerable: false, + value: reload, + writable: true + }) + + Object.defineProperty(sess, 'save', { + configurable: true, + enumerable: false, + value: save, + writable: true + }); + } + + // check if session has been modified + function isModified(sess) { + return originalId !== sess.id || originalHash !== hash(sess); + } + + // check if session has been saved + function isSaved(sess) { + return originalId === sess.id && savedHash === hash(sess); + } + + // determine if session should be destroyed + function shouldDestroy(req) { + return req.sessionID && unsetDestroy && req.session == null; + } + + // determine if session should be saved to store + function shouldSave(req) { + // cannot set cookie without a session ID + if (typeof req.sessionID !== 'string') { + debug('session ignored because of bogus req.sessionID %o', req.sessionID); + return false; + } + + return !saveUninitializedSession && !savedHash && cookieId !== req.sessionID + ? isModified(req.session) + : !isSaved(req.session) + } + + // determine if session should be touched + function shouldTouch(req) { + // cannot set cookie without a session ID + if (typeof req.sessionID !== 'string') { + debug('session ignored because of bogus req.sessionID %o', req.sessionID); + return false; + } + + return cookieId === req.sessionID && !shouldSave(req); + } + + // determine if cookie should be set on response + function shouldSetCookie(req) { + // cannot set cookie without a session ID + if (typeof req.sessionID !== 'string') { + return false; + } + + return cookieId !== req.sessionID + ? saveUninitializedSession || isModified(req.session) + : rollingSessions || req.session.cookie.expires != null && isModified(req.session); + } + + // generate a session if the browser doesn't send a sessionID + if (!req.sessionID) { + debug('no SID sent, generating session'); + generate(); + next(); + return; + } + + // generate the session object + debug('fetching %s', req.sessionID); + store.get(req.sessionID, function(err, sess){ + // error handling + if (err && err.code !== 'ENOENT') { + debug('error %j', err); + next(err) + return + } + + try { + if (err || !sess) { + debug('no session found') + generate() + } else { + debug('session found') + inflate(req, sess) + } + } catch (e) { + next(e) + return + } + + next() + }); + }; +}; + +/** + * Generate a session ID for a new session. + * + * @return {String} + * @private + */ + +function generateSessionId(sess) { + return uid(24); +} + +/** + * Get the session ID cookie from request. + * + * @return {string} + * @private + */ + +function getcookie(req, name, secrets) { + var header = req.headers.cookie; + var raw; + var val; + + // read from cookie header + if (header) { + var cookies = cookie.parse(header); + + raw = cookies[name]; + + if (raw) { + if (raw.substr(0, 2) === 's:') { + val = unsigncookie(raw.slice(2), secrets); + + if (val === false) { + debug('cookie signature invalid'); + val = undefined; + } + } else { + debug('cookie unsigned') + } + } + } + + // back-compat read from cookieParser() signedCookies data + if (!val && req.signedCookies) { + val = req.signedCookies[name]; + + if (val) { + deprecate('cookie should be available in req.headers.cookie'); + } + } + + // back-compat read from cookieParser() cookies data + if (!val && req.cookies) { + raw = req.cookies[name]; + + if (raw) { + if (raw.substr(0, 2) === 's:') { + val = unsigncookie(raw.slice(2), secrets); + + if (val) { + deprecate('cookie should be available in req.headers.cookie'); + } + + if (val === false) { + debug('cookie signature invalid'); + val = undefined; + } + } else { + debug('cookie unsigned') + } + } + } + + return val; +} + +/** + * Hash the given `sess` object omitting changes to `.cookie`. + * + * @param {Object} sess + * @return {String} + * @private + */ + +function hash(sess) { + // serialize + var str = JSON.stringify(sess, function (key, val) { + // ignore sess.cookie property + if (this === sess && key === 'cookie') { + return + } + + return val + }) + + // hash + return crypto + .createHash('sha1') + .update(str, 'utf8') + .digest('hex') +} + +/** + * Determine if request is secure. + * + * @param {Object} req + * @param {Boolean} [trustProxy] + * @return {Boolean} + * @private + */ + +function issecure(req, trustProxy) { + // socket is https server + if (req.connection && req.connection.encrypted) { + return true; + } + + // do not trust proxy + if (trustProxy === false) { + return false; + } + + // no explicit trust; try req.secure from express + if (trustProxy !== true) { + return req.secure === true + } + + // read the proto from x-forwarded-proto header + var header = req.headers['x-forwarded-proto'] || ''; + var index = header.indexOf(','); + var proto = index !== -1 + ? header.substr(0, index).toLowerCase().trim() + : header.toLowerCase().trim() + + return proto === 'https'; +} + +/** + * Set cookie on response. + * + * @private + */ + +function setcookie(res, name, val, secret, options) { + var signed = 's:' + signature.sign(val, secret); + var data = cookie.serialize(name, signed, options); + + debug('set-cookie %s', data); + + var prev = res.getHeader('Set-Cookie') || [] + var header = Array.isArray(prev) ? prev.concat(data) : [prev, data]; + + res.setHeader('Set-Cookie', header) +} + +/** + * Verify and decode the given `val` with `secrets`. + * + * @param {String} val + * @param {Array} secrets + * @returns {String|Boolean} + * @private + */ +function unsigncookie(val, secrets) { + for (var i = 0; i < secrets.length; i++) { + var result = signature.unsign(val, secrets[i]); + + if (result !== false) { + return result; + } + } + + return false; +} diff --git a/node_modules/express-session/node_modules/cookie-signature/index.js b/node_modules/express-session/node_modules/cookie-signature/index.js new file mode 100644 index 0000000..336d487 --- /dev/null +++ b/node_modules/express-session/node_modules/cookie-signature/index.js @@ -0,0 +1,51 @@ +/** + * Module dependencies. + */ + +var crypto = require('crypto'); + +/** + * Sign the given `val` with `secret`. + * + * @param {String} val + * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret + * @return {String} + * @api private + */ + +exports.sign = function(val, secret){ + if ('string' !== typeof val) throw new TypeError("Cookie value must be provided as a string."); + if (null == secret) throw new TypeError("Secret key must be provided."); + return val + '.' + crypto + .createHmac('sha256', secret) + .update(val) + .digest('base64') + .replace(/\=+$/, ''); +}; + +/** + * Unsign and decode the given `val` with `secret`, + * returning `false` if the signature is invalid. + * + * @param {String} val + * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret + * @return {String|Boolean} + * @api private + */ + +exports.unsign = function(val, secret){ + if ('string' !== typeof val) throw new TypeError("Signed cookie string must be provided."); + if (null == secret) throw new TypeError("Secret key must be provided."); + var str = val.slice(0, val.lastIndexOf('.')) + , mac = exports.sign(str, secret); + + return sha1(mac) == sha1(val) ? str : false; +}; + +/** + * Private + */ + +function sha1(str){ + return crypto.createHash('sha1').update(str).digest('hex'); +} diff --git a/node_modules/express-session/node_modules/cookie-signature/package.json b/node_modules/express-session/node_modules/cookie-signature/package.json new file mode 100644 index 0000000..738487b --- /dev/null +++ b/node_modules/express-session/node_modules/cookie-signature/package.json @@ -0,0 +1,18 @@ +{ + "name": "cookie-signature", + "version": "1.0.7", + "description": "Sign and unsign cookies", + "keywords": ["cookie", "sign", "unsign"], + "author": "TJ Holowaychuk <tj@learnboost.com>", + "license": "MIT", + "repository": { "type": "git", "url": "https://github.com/visionmedia/node-cookie-signature.git"}, + "dependencies": {}, + "devDependencies": { + "mocha": "*", + "should": "*" + }, + "scripts": { + "test": "mocha --require should --reporter spec" + }, + "main": "index" +} \ No newline at end of file diff --git a/node_modules/express-session/node_modules/cookie/index.js b/node_modules/express-session/node_modules/cookie/index.js new file mode 100644 index 0000000..acd5acd --- /dev/null +++ b/node_modules/express-session/node_modules/cookie/index.js @@ -0,0 +1,335 @@ +/*! + * cookie + * Copyright(c) 2012-2014 Roman Shtylman + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module exports. + * @public + */ + +exports.parse = parse; +exports.serialize = serialize; + +/** + * Module variables. + * @private + */ + +var __toString = Object.prototype.toString +var __hasOwnProperty = Object.prototype.hasOwnProperty + +/** + * RegExp to match cookie-name in RFC 6265 sec 4.1.1 + * This refers out to the obsoleted definition of token in RFC 2616 sec 2.2 + * which has been replaced by the token definition in RFC 7230 appendix B. + * + * cookie-name = token + * token = 1*tchar + * tchar = "!" / "#" / "$" / "%" / "&" / "'" / + * "*" / "+" / "-" / "." / "^" / "_" / + * "`" / "|" / "~" / DIGIT / ALPHA + */ + +var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + +/** + * RegExp to match cookie-value in RFC 6265 sec 4.1.1 + * + * cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + * ; US-ASCII characters excluding CTLs, + * ; whitespace DQUOTE, comma, semicolon, + * ; and backslash + */ + +var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/; + +/** + * RegExp to match domain-value in RFC 6265 sec 4.1.1 + * + * domain-value = <subdomain> + * ; defined in [RFC1034], Section 3.5, as + * ; enhanced by [RFC1123], Section 2.1 + * <subdomain> = <label> | <subdomain> "." <label> + * <label> = <let-dig> [ [ <ldh-str> ] <let-dig> ] + * Labels must be 63 characters or less. + * 'let-dig' not 'letter' in the first char, per RFC1123 + * <ldh-str> = <let-dig-hyp> | <let-dig-hyp> <ldh-str> + * <let-dig-hyp> = <let-dig> | "-" + * <let-dig> = <letter> | <digit> + * <letter> = any one of the 52 alphabetic characters A through Z in + * upper case and a through z in lower case + * <digit> = any one of the ten digits 0 through 9 + * + * Keep support for leading dot: https://github.com/jshttp/cookie/issues/173 + * + * > (Note that a leading %x2E ("."), if present, is ignored even though that + * character is not permitted, but a trailing %x2E ("."), if present, will + * cause the user agent to ignore the attribute.) + */ + +var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i; + +/** + * RegExp to match path-value in RFC 6265 sec 4.1.1 + * + * path-value = <any CHAR except CTLs or ";"> + * CHAR = %x01-7F + * ; defined in RFC 5234 appendix B.1 + */ + +var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/; + +/** + * Parse a cookie header. + * + * Parse the given cookie header string into an object + * The object has the various cookies as keys(names) => values + * + * @param {string} str + * @param {object} [opt] + * @return {object} + * @public + */ + +function parse(str, opt) { + if (typeof str !== 'string') { + throw new TypeError('argument str must be a string'); + } + + var obj = {}; + var len = str.length; + // RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='. + if (len < 2) return obj; + + var dec = (opt && opt.decode) || decode; + var index = 0; + var eqIdx = 0; + var endIdx = 0; + + do { + eqIdx = str.indexOf('=', index); + if (eqIdx === -1) break; // No more cookie pairs. + + endIdx = str.indexOf(';', index); + + if (endIdx === -1) { + endIdx = len; + } else if (eqIdx > endIdx) { + // backtrack on prior semicolon + index = str.lastIndexOf(';', eqIdx - 1) + 1; + continue; + } + + var keyStartIdx = startIndex(str, index, eqIdx); + var keyEndIdx = endIndex(str, eqIdx, keyStartIdx); + var key = str.slice(keyStartIdx, keyEndIdx); + + // only assign once + if (!__hasOwnProperty.call(obj, key)) { + var valStartIdx = startIndex(str, eqIdx + 1, endIdx); + var valEndIdx = endIndex(str, endIdx, valStartIdx); + + if (str.charCodeAt(valStartIdx) === 0x22 /* " */ && str.charCodeAt(valEndIdx - 1) === 0x22 /* " */) { + valStartIdx++; + valEndIdx--; + } + + var val = str.slice(valStartIdx, valEndIdx); + obj[key] = tryDecode(val, dec); + } + + index = endIdx + 1 + } while (index < len); + + return obj; +} + +function startIndex(str, index, max) { + do { + var code = str.charCodeAt(index); + if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index; + } while (++index < max); + return max; +} + +function endIndex(str, index, min) { + while (index > min) { + var code = str.charCodeAt(--index); + if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index + 1; + } + return min; +} + +/** + * Serialize data into a cookie header. + * + * Serialize a name value pair into a cookie string suitable for + * http headers. An optional options object specifies cookie parameters. + * + * serialize('foo', 'bar', { httpOnly: true }) + * => "foo=bar; httpOnly" + * + * @param {string} name + * @param {string} val + * @param {object} [opt] + * @return {string} + * @public + */ + +function serialize(name, val, opt) { + var enc = (opt && opt.encode) || encodeURIComponent; + + if (typeof enc !== 'function') { + throw new TypeError('option encode is invalid'); + } + + if (!cookieNameRegExp.test(name)) { + throw new TypeError('argument name is invalid'); + } + + var value = enc(val); + + if (!cookieValueRegExp.test(value)) { + throw new TypeError('argument val is invalid'); + } + + var str = name + '=' + value; + if (!opt) return str; + + if (null != opt.maxAge) { + var maxAge = Math.floor(opt.maxAge); + + if (!isFinite(maxAge)) { + throw new TypeError('option maxAge is invalid') + } + + str += '; Max-Age=' + maxAge; + } + + if (opt.domain) { + if (!domainValueRegExp.test(opt.domain)) { + throw new TypeError('option domain is invalid'); + } + + str += '; Domain=' + opt.domain; + } + + if (opt.path) { + if (!pathValueRegExp.test(opt.path)) { + throw new TypeError('option path is invalid'); + } + + str += '; Path=' + opt.path; + } + + if (opt.expires) { + var expires = opt.expires + + if (!isDate(expires) || isNaN(expires.valueOf())) { + throw new TypeError('option expires is invalid'); + } + + str += '; Expires=' + expires.toUTCString() + } + + if (opt.httpOnly) { + str += '; HttpOnly'; + } + + if (opt.secure) { + str += '; Secure'; + } + + if (opt.partitioned) { + str += '; Partitioned' + } + + if (opt.priority) { + var priority = typeof opt.priority === 'string' + ? opt.priority.toLowerCase() : opt.priority; + + switch (priority) { + case 'low': + str += '; Priority=Low' + break + case 'medium': + str += '; Priority=Medium' + break + case 'high': + str += '; Priority=High' + break + default: + throw new TypeError('option priority is invalid') + } + } + + if (opt.sameSite) { + var sameSite = typeof opt.sameSite === 'string' + ? opt.sameSite.toLowerCase() : opt.sameSite; + + switch (sameSite) { + case true: + str += '; SameSite=Strict'; + break; + case 'lax': + str += '; SameSite=Lax'; + break; + case 'strict': + str += '; SameSite=Strict'; + break; + case 'none': + str += '; SameSite=None'; + break; + default: + throw new TypeError('option sameSite is invalid'); + } + } + + return str; +} + +/** + * URL-decode string value. Optimized to skip native call when no %. + * + * @param {string} str + * @returns {string} + */ + +function decode (str) { + return str.indexOf('%') !== -1 + ? decodeURIComponent(str) + : str +} + +/** + * Determine if value is a Date. + * + * @param {*} val + * @private + */ + +function isDate (val) { + return __toString.call(val) === '[object Date]'; +} + +/** + * Try decoding a string using a decoding function. + * + * @param {string} str + * @param {function} decode + * @private + */ + +function tryDecode(str, decode) { + try { + return decode(str); + } catch (e) { + return str; + } +} diff --git a/node_modules/express-session/node_modules/cookie/package.json b/node_modules/express-session/node_modules/cookie/package.json new file mode 100644 index 0000000..22e3f92 --- /dev/null +++ b/node_modules/express-session/node_modules/cookie/package.json @@ -0,0 +1,44 @@ +{ + "name": "cookie", + "description": "HTTP server cookie parsing and serialization", + "version": "0.7.2", + "author": "Roman Shtylman <shtylman@gmail.com>", + "contributors": [ + "Douglas Christopher Wilson <doug@somethingdoug.com>" + ], + "license": "MIT", + "keywords": [ + "cookie", + "cookies" + ], + "repository": "jshttp/cookie", + "devDependencies": { + "beautify-benchmark": "0.2.4", + "benchmark": "2.1.4", + "eslint": "8.53.0", + "eslint-plugin-markdown": "3.0.1", + "mocha": "10.2.0", + "nyc": "15.1.0", + "safe-buffer": "5.2.1", + "top-sites": "1.1.194" + }, + "files": [ + "HISTORY.md", + "LICENSE", + "README.md", + "SECURITY.md", + "index.js" + ], + "main": "index.js", + "engines": { + "node": ">= 0.6" + }, + "scripts": { + "bench": "node benchmark/index.js", + "lint": "eslint .", + "test": "mocha --reporter spec --bail --check-leaks test/", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test", + "update-bench": "node scripts/update-benchmark.js" + } +} diff --git a/node_modules/express-session/package.json b/node_modules/express-session/package.json new file mode 100644 index 0000000..e332243 --- /dev/null +++ b/node_modules/express-session/package.json @@ -0,0 +1,47 @@ +{ + "name": "express-session", + "version": "1.18.1", + "description": "Simple session middleware for Express", + "author": "TJ Holowaychuk <tj@vision-media.ca> (http://tjholowaychuk.com)", + "contributors": [ + "Douglas Christopher Wilson <doug@somethingdoug.com>", + "Joe Wagner <njwjs722@gmail.com>" + ], + "repository": "expressjs/session", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "devDependencies": { + "after": "0.8.2", + "cookie-parser": "1.4.6", + "eslint": "8.56.0", + "eslint-plugin-markdown": "3.0.1", + "express": "4.17.3", + "mocha": "10.2.0", + "nyc": "15.1.0", + "supertest": "6.3.4" + }, + "files": [ + "session/", + "HISTORY.md", + "index.js" + ], + "engines": { + "node": ">= 0.8.0" + }, + "scripts": { + "lint": "eslint . && node ./scripts/lint-readme.js", + "test": "mocha --require test/support/env --check-leaks --bail --no-exit --reporter spec test/", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc npm test", + "version": "node scripts/version-history.js && git add HISTORY.md" + } +} diff --git a/node_modules/express-session/session/cookie.js b/node_modules/express-session/session/cookie.js new file mode 100644 index 0000000..8bb5907 --- /dev/null +++ b/node_modules/express-session/session/cookie.js @@ -0,0 +1,152 @@ +/*! + * Connect - session - Cookie + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + */ + +var cookie = require('cookie') +var deprecate = require('depd')('express-session') + +/** + * Initialize a new `Cookie` with the given `options`. + * + * @param {IncomingMessage} req + * @param {Object} options + * @api private + */ + +var Cookie = module.exports = function Cookie(options) { + this.path = '/'; + this.maxAge = null; + this.httpOnly = true; + + if (options) { + if (typeof options !== 'object') { + throw new TypeError('argument options must be a object') + } + + for (var key in options) { + if (key !== 'data') { + this[key] = options[key] + } + } + } + + if (this.originalMaxAge === undefined || this.originalMaxAge === null) { + this.originalMaxAge = this.maxAge + } +}; + +/*! + * Prototype. + */ + +Cookie.prototype = { + + /** + * Set expires `date`. + * + * @param {Date} date + * @api public + */ + + set expires(date) { + this._expires = date; + this.originalMaxAge = this.maxAge; + }, + + /** + * Get expires `date`. + * + * @return {Date} + * @api public + */ + + get expires() { + return this._expires; + }, + + /** + * Set expires via max-age in `ms`. + * + * @param {Number} ms + * @api public + */ + + set maxAge(ms) { + if (ms && typeof ms !== 'number' && !(ms instanceof Date)) { + throw new TypeError('maxAge must be a number or Date') + } + + if (ms instanceof Date) { + deprecate('maxAge as Date; pass number of milliseconds instead') + } + + this.expires = typeof ms === 'number' + ? new Date(Date.now() + ms) + : ms; + }, + + /** + * Get expires max-age in `ms`. + * + * @return {Number} + * @api public + */ + + get maxAge() { + return this.expires instanceof Date + ? this.expires.valueOf() - Date.now() + : this.expires; + }, + + /** + * Return cookie data object. + * + * @return {Object} + * @api private + */ + + get data() { + return { + originalMaxAge: this.originalMaxAge, + partitioned: this.partitioned, + priority: this.priority + , expires: this._expires + , secure: this.secure + , httpOnly: this.httpOnly + , domain: this.domain + , path: this.path + , sameSite: this.sameSite + } + }, + + /** + * Return a serialized cookie string. + * + * @return {String} + * @api public + */ + + serialize: function(name, val){ + return cookie.serialize(name, val, this.data); + }, + + /** + * Return JSON representation of this cookie. + * + * @return {Object} + * @api private + */ + + toJSON: function(){ + return this.data; + } +}; diff --git a/node_modules/express-session/session/memory.js b/node_modules/express-session/session/memory.js new file mode 100644 index 0000000..11ed686 --- /dev/null +++ b/node_modules/express-session/session/memory.js @@ -0,0 +1,187 @@ +/*! + * express-session + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var Store = require('./store') +var util = require('util') + +/** + * Shim setImmediate for node.js < 0.10 + * @private + */ + +/* istanbul ignore next */ +var defer = typeof setImmediate === 'function' + ? setImmediate + : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } + +/** + * Module exports. + */ + +module.exports = MemoryStore + +/** + * A session store in memory. + * @public + */ + +function MemoryStore() { + Store.call(this) + this.sessions = Object.create(null) +} + +/** + * Inherit from Store. + */ + +util.inherits(MemoryStore, Store) + +/** + * Get all active sessions. + * + * @param {function} callback + * @public + */ + +MemoryStore.prototype.all = function all(callback) { + var sessionIds = Object.keys(this.sessions) + var sessions = Object.create(null) + + for (var i = 0; i < sessionIds.length; i++) { + var sessionId = sessionIds[i] + var session = getSession.call(this, sessionId) + + if (session) { + sessions[sessionId] = session; + } + } + + callback && defer(callback, null, sessions) +} + +/** + * Clear all sessions. + * + * @param {function} callback + * @public + */ + +MemoryStore.prototype.clear = function clear(callback) { + this.sessions = Object.create(null) + callback && defer(callback) +} + +/** + * Destroy the session associated with the given session ID. + * + * @param {string} sessionId + * @public + */ + +MemoryStore.prototype.destroy = function destroy(sessionId, callback) { + delete this.sessions[sessionId] + callback && defer(callback) +} + +/** + * Fetch session by the given session ID. + * + * @param {string} sessionId + * @param {function} callback + * @public + */ + +MemoryStore.prototype.get = function get(sessionId, callback) { + defer(callback, null, getSession.call(this, sessionId)) +} + +/** + * Commit the given session associated with the given sessionId to the store. + * + * @param {string} sessionId + * @param {object} session + * @param {function} callback + * @public + */ + +MemoryStore.prototype.set = function set(sessionId, session, callback) { + this.sessions[sessionId] = JSON.stringify(session) + callback && defer(callback) +} + +/** + * Get number of active sessions. + * + * @param {function} callback + * @public + */ + +MemoryStore.prototype.length = function length(callback) { + this.all(function (err, sessions) { + if (err) return callback(err) + callback(null, Object.keys(sessions).length) + }) +} + +/** + * Touch the given session object associated with the given session ID. + * + * @param {string} sessionId + * @param {object} session + * @param {function} callback + * @public + */ + +MemoryStore.prototype.touch = function touch(sessionId, session, callback) { + var currentSession = getSession.call(this, sessionId) + + if (currentSession) { + // update expiration + currentSession.cookie = session.cookie + this.sessions[sessionId] = JSON.stringify(currentSession) + } + + callback && defer(callback) +} + +/** + * Get session from the store. + * @private + */ + +function getSession(sessionId) { + var sess = this.sessions[sessionId] + + if (!sess) { + return + } + + // parse + sess = JSON.parse(sess) + + if (sess.cookie) { + var expires = typeof sess.cookie.expires === 'string' + ? new Date(sess.cookie.expires) + : sess.cookie.expires + + // destroy expired session + if (expires && expires <= Date.now()) { + delete this.sessions[sessionId] + return + } + } + + return sess +} diff --git a/node_modules/express-session/session/session.js b/node_modules/express-session/session/session.js new file mode 100644 index 0000000..fee7608 --- /dev/null +++ b/node_modules/express-session/session/session.js @@ -0,0 +1,143 @@ +/*! + * Connect - session - Session + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +'use strict'; + +/** + * Expose Session. + */ + +module.exports = Session; + +/** + * Create a new `Session` with the given request and `data`. + * + * @param {IncomingRequest} req + * @param {Object} data + * @api private + */ + +function Session(req, data) { + Object.defineProperty(this, 'req', { value: req }); + Object.defineProperty(this, 'id', { value: req.sessionID }); + + if (typeof data === 'object' && data !== null) { + // merge data into this, ignoring prototype properties + for (var prop in data) { + if (!(prop in this)) { + this[prop] = data[prop] + } + } + } +} + +/** + * Update reset `.cookie.maxAge` to prevent + * the cookie from expiring when the + * session is still active. + * + * @return {Session} for chaining + * @api public + */ + +defineMethod(Session.prototype, 'touch', function touch() { + return this.resetMaxAge(); +}); + +/** + * Reset `.maxAge` to `.originalMaxAge`. + * + * @return {Session} for chaining + * @api public + */ + +defineMethod(Session.prototype, 'resetMaxAge', function resetMaxAge() { + this.cookie.maxAge = this.cookie.originalMaxAge; + return this; +}); + +/** + * Save the session data with optional callback `fn(err)`. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +defineMethod(Session.prototype, 'save', function save(fn) { + this.req.sessionStore.set(this.id, this, fn || function(){}); + return this; +}); + +/** + * Re-loads the session data _without_ altering + * the maxAge properties. Invokes the callback `fn(err)`, + * after which time if no exception has occurred the + * `req.session` property will be a new `Session` object, + * although representing the same session. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +defineMethod(Session.prototype, 'reload', function reload(fn) { + var req = this.req + var store = this.req.sessionStore + + store.get(this.id, function(err, sess){ + if (err) return fn(err); + if (!sess) return fn(new Error('failed to load session')); + store.createSession(req, sess); + fn(); + }); + return this; +}); + +/** + * Destroy `this` session. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +defineMethod(Session.prototype, 'destroy', function destroy(fn) { + delete this.req.session; + this.req.sessionStore.destroy(this.id, fn); + return this; +}); + +/** + * Regenerate this request's session. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +defineMethod(Session.prototype, 'regenerate', function regenerate(fn) { + this.req.sessionStore.regenerate(this.req, fn); + return this; +}); + +/** + * Helper function for creating a method on a prototype. + * + * @param {Object} obj + * @param {String} name + * @param {Function} fn + * @private + */ +function defineMethod(obj, name, fn) { + Object.defineProperty(obj, name, { + configurable: true, + enumerable: false, + value: fn, + writable: true + }); +}; diff --git a/node_modules/express-session/session/store.js b/node_modules/express-session/session/store.js new file mode 100644 index 0000000..3793877 --- /dev/null +++ b/node_modules/express-session/session/store.js @@ -0,0 +1,102 @@ +/*! + * Connect - session - Store + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var Cookie = require('./cookie') +var EventEmitter = require('events').EventEmitter +var Session = require('./session') +var util = require('util') + +/** + * Module exports. + * @public + */ + +module.exports = Store + +/** + * Abstract base class for session stores. + * @public + */ + +function Store () { + EventEmitter.call(this) +} + +/** + * Inherit from EventEmitter. + */ + +util.inherits(Store, EventEmitter) + +/** + * Re-generate the given requests's session. + * + * @param {IncomingRequest} req + * @return {Function} fn + * @api public + */ + +Store.prototype.regenerate = function(req, fn){ + var self = this; + this.destroy(req.sessionID, function(err){ + self.generate(req); + fn(err); + }); +}; + +/** + * Load a `Session` instance via the given `sid` + * and invoke the callback `fn(err, sess)`. + * + * @param {String} sid + * @param {Function} fn + * @api public + */ + +Store.prototype.load = function(sid, fn){ + var self = this; + this.get(sid, function(err, sess){ + if (err) return fn(err); + if (!sess) return fn(); + var req = { sessionID: sid, sessionStore: self }; + fn(null, self.createSession(req, sess)) + }); +}; + +/** + * Create session from JSON `sess` data. + * + * @param {IncomingRequest} req + * @param {Object} sess + * @return {Session} + * @api private + */ + +Store.prototype.createSession = function(req, sess){ + var expires = sess.cookie.expires + var originalMaxAge = sess.cookie.originalMaxAge + + sess.cookie = new Cookie(sess.cookie); + + if (typeof expires === 'string') { + // convert expires to a Date object + sess.cookie.expires = new Date(expires) + } + + // keep originalMaxAge intact + sess.cookie.originalMaxAge = originalMaxAge + + req.session = new Session(req, sess); + return req.session; +}; diff --git a/node_modules/on-headers/index.js b/node_modules/on-headers/index.js new file mode 100644 index 0000000..7db6375 --- /dev/null +++ b/node_modules/on-headers/index.js @@ -0,0 +1,132 @@ +/*! + * on-headers + * Copyright(c) 2014 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +module.exports = onHeaders + +/** + * Create a replacement writeHead method. + * + * @param {function} prevWriteHead + * @param {function} listener + * @private + */ + +function createWriteHead (prevWriteHead, listener) { + var fired = false + + // return function with core name and argument list + return function writeHead (statusCode) { + // set headers from arguments + var args = setWriteHeadHeaders.apply(this, arguments) + + // fire listener + if (!fired) { + fired = true + listener.call(this) + + // pass-along an updated status code + if (typeof args[0] === 'number' && this.statusCode !== args[0]) { + args[0] = this.statusCode + args.length = 1 + } + } + + return prevWriteHead.apply(this, args) + } +} + +/** + * Execute a listener when a response is about to write headers. + * + * @param {object} res + * @return {function} listener + * @public + */ + +function onHeaders (res, listener) { + if (!res) { + throw new TypeError('argument res is required') + } + + if (typeof listener !== 'function') { + throw new TypeError('argument listener must be a function') + } + + res.writeHead = createWriteHead(res.writeHead, listener) +} + +/** + * Set headers contained in array on the response object. + * + * @param {object} res + * @param {array} headers + * @private + */ + +function setHeadersFromArray (res, headers) { + for (var i = 0; i < headers.length; i++) { + res.setHeader(headers[i][0], headers[i][1]) + } +} + +/** + * Set headers contained in object on the response object. + * + * @param {object} res + * @param {object} headers + * @private + */ + +function setHeadersFromObject (res, headers) { + var keys = Object.keys(headers) + for (var i = 0; i < keys.length; i++) { + var k = keys[i] + if (k) res.setHeader(k, headers[k]) + } +} + +/** + * Set headers and other properties on the response object. + * + * @param {number} statusCode + * @private + */ + +function setWriteHeadHeaders (statusCode) { + var length = arguments.length + var headerIndex = length > 1 && typeof arguments[1] === 'string' + ? 2 + : 1 + + var headers = length >= headerIndex + 1 + ? arguments[headerIndex] + : undefined + + this.statusCode = statusCode + + if (Array.isArray(headers)) { + // handle array case + setHeadersFromArray(this, headers) + } else if (headers) { + // handle object case + setHeadersFromObject(this, headers) + } + + // copy leading arguments + var args = new Array(Math.min(length, headerIndex)) + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i] + } + + return args +} diff --git a/node_modules/on-headers/package.json b/node_modules/on-headers/package.json new file mode 100644 index 0000000..1e9bf9e --- /dev/null +++ b/node_modules/on-headers/package.json @@ -0,0 +1,42 @@ +{ + "name": "on-headers", + "description": "Execute a listener when a response is about to write headers", + "version": "1.0.2", + "author": "Douglas Christopher Wilson <doug@somethingdoug.com>", + "license": "MIT", + "keywords": [ + "event", + "headers", + "http", + "onheaders" + ], + "repository": "jshttp/on-headers", + "devDependencies": { + "eslint": "5.14.1", + "eslint-config-standard": "12.0.0", + "eslint-plugin-import": "2.16.0", + "eslint-plugin-markdown": "1.0.0", + "eslint-plugin-node": "8.0.1", + "eslint-plugin-promise": "4.0.1", + "eslint-plugin-standard": "4.0.0", + "istanbul": "0.4.5", + "mocha": "6.0.1", + "supertest": "3.4.2" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "lint": "eslint --plugin markdown --ext js,md .", + "test": "mocha --reporter spec --bail --check-leaks test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/", + "version": "node scripts/version-history.js && git add HISTORY.md" + } +} diff --git a/node_modules/random-bytes/index.js b/node_modules/random-bytes/index.js new file mode 100644 index 0000000..9ad930f --- /dev/null +++ b/node_modules/random-bytes/index.js @@ -0,0 +1,101 @@ +/*! + * random-bytes + * Copyright(c) 2016 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var crypto = require('crypto') + +/** + * Module variables. + * @private + */ + +var generateAttempts = crypto.randomBytes === crypto.pseudoRandomBytes ? 1 : 3 + +/** + * Module exports. + * @public + */ + +module.exports = randomBytes +module.exports.sync = randomBytesSync + +/** + * Generates strong pseudo-random bytes. + * + * @param {number} size + * @param {function} [callback] + * @return {Promise} + * @public + */ + +function randomBytes(size, callback) { + // validate callback is a function, if provided + if (callback !== undefined && typeof callback !== 'function') { + throw new TypeError('argument callback must be a function') + } + + // require the callback without promises + if (!callback && !global.Promise) { + throw new TypeError('argument callback is required') + } + + if (callback) { + // classic callback style + return generateRandomBytes(size, generateAttempts, callback) + } + + return new Promise(function executor(resolve, reject) { + generateRandomBytes(size, generateAttempts, function onRandomBytes(err, str) { + if (err) return reject(err) + resolve(str) + }) + }) +} + +/** + * Generates strong pseudo-random bytes sync. + * + * @param {number} size + * @return {Buffer} + * @public + */ + +function randomBytesSync(size) { + var err = null + + for (var i = 0; i < generateAttempts; i++) { + try { + return crypto.randomBytes(size) + } catch (e) { + err = e + } + } + + throw err +} + +/** + * Generates strong pseudo-random bytes. + * + * @param {number} size + * @param {number} attempts + * @param {function} callback + * @private + */ + +function generateRandomBytes(size, attempts, callback) { + crypto.randomBytes(size, function onRandomBytes(err, buf) { + if (!err) return callback(null, buf) + if (!--attempts) return callback(err) + setTimeout(generateRandomBytes.bind(null, size, attempts, callback), 10) + }) +} diff --git a/node_modules/random-bytes/package.json b/node_modules/random-bytes/package.json new file mode 100644 index 0000000..c67e0e8 --- /dev/null +++ b/node_modules/random-bytes/package.json @@ -0,0 +1,36 @@ +{ + "name": "random-bytes", + "description": "URL and cookie safe UIDs", + "version": "1.0.0", + "contributors": [ + "Douglas Christopher Wilson <doug@somethingdoug.com>" + ], + "license": "MIT", + "repository": "crypto-utils/random-bytes", + "devDependencies": { + "bluebird": "3.1.1", + "istanbul": "0.4.2", + "mocha": "2.3.4", + "proxyquire": "1.2.0" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "test": "mocha --trace-deprecation --reporter spec --bail --check-leaks test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --trace-deprecation --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --trace-deprecation --reporter spec --check-leaks test/" + }, + "keywords": [ + "bytes", + "generator", + "random", + "safe" + ] +} diff --git a/node_modules/uid-safe/index.js b/node_modules/uid-safe/index.js new file mode 100644 index 0000000..18e8492 --- /dev/null +++ b/node_modules/uid-safe/index.js @@ -0,0 +1,107 @@ +/*! + * uid-safe + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015-2017 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var randomBytes = require('random-bytes') + +/** + * Module variables. + * @private + */ + +var EQUAL_END_REGEXP = /=+$/ +var PLUS_GLOBAL_REGEXP = /\+/g +var SLASH_GLOBAL_REGEXP = /\//g + +/** + * Module exports. + * @public + */ + +module.exports = uid +module.exports.sync = uidSync + +/** + * Create a unique ID. + * + * @param {number} length + * @param {function} [callback] + * @return {Promise} + * @public + */ + +function uid (length, callback) { + // validate callback is a function, if provided + if (callback !== undefined && typeof callback !== 'function') { + throw new TypeError('argument callback must be a function') + } + + // require the callback without promises + if (!callback && !global.Promise) { + throw new TypeError('argument callback is required') + } + + if (callback) { + // classic callback style + return generateUid(length, callback) + } + + return new Promise(function executor (resolve, reject) { + generateUid(length, function onUid (err, str) { + if (err) return reject(err) + resolve(str) + }) + }) +} + +/** + * Create a unique ID sync. + * + * @param {number} length + * @return {string} + * @public + */ + +function uidSync (length) { + return toString(randomBytes.sync(length)) +} + +/** + * Generate a unique ID string. + * + * @param {number} length + * @param {function} callback + * @private + */ + +function generateUid (length, callback) { + randomBytes(length, function (err, buf) { + if (err) return callback(err) + callback(null, toString(buf)) + }) +} + +/** + * Change a Buffer into a string. + * + * @param {Buffer} buf + * @return {string} + * @private + */ + +function toString (buf) { + return buf.toString('base64') + .replace(EQUAL_END_REGEXP, '') + .replace(PLUS_GLOBAL_REGEXP, '-') + .replace(SLASH_GLOBAL_REGEXP, '_') +} diff --git a/node_modules/uid-safe/package.json b/node_modules/uid-safe/package.json new file mode 100644 index 0000000..9d9ea4c --- /dev/null +++ b/node_modules/uid-safe/package.json @@ -0,0 +1,46 @@ +{ + "name": "uid-safe", + "description": "URL and cookie safe UIDs", + "version": "2.1.5", + "contributors": [ + "Douglas Christopher Wilson <doug@somethingdoug.com>", + "Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)" + ], + "license": "MIT", + "repository": "crypto-utils/uid-safe", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "devDependencies": { + "bluebird": "3.5.0", + "eslint": "3.19.0", + "eslint-config-standard": "10.2.1", + "eslint-plugin-import": "2.7.0", + "eslint-plugin-node": "5.1.1", + "eslint-plugin-promise": "3.5.0", + "eslint-plugin-standard": "3.0.1", + "istanbul": "0.4.5", + "mocha": "2.5.3" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --trace-deprecation --reporter spec --bail --check-leaks test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --trace-deprecation --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --trace-deprecation --reporter spec --check-leaks test/" + }, + "keywords": [ + "random", + "generator", + "uid", + "safe" + ] +} diff --git a/package-lock.json b/package-lock.json index 4cd596f..e3f6cda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,11 @@ "version": "1.0.0", "dependencies": { "body-parser": "^2.2.0", + "call-of-duty-api": "^4.1.0", "csso": "^5.0.5", "express": "^4.21.2", + "express-rate-limit": "^7.5.0", + "express-session": "^1.18.1", "glob": "^11.0.1", "html-minifier": "^4.0.0", "pkg": "^5.8.1", @@ -82,6 +85,15 @@ "node": ">=6.9.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -528,6 +540,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-of-duty-api": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/call-of-duty-api/-/call-of-duty-api-4.1.0.tgz", + "integrity": "sha512-f5DJ6gQru6f406QVBZkkXOv0gUzFu0hykdkyKRa2Am6iWwGRVzcBK7rq+xHpNI6oTq3EFfw0T70kb38rZaezNA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0", + "undici": "^5.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Lierrmm" + } + }, + "node_modules/call-of-duty-api/node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/camel-case": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", @@ -1007,6 +1046,55 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/express/node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1889,6 +1977,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2151,6 +2248,15 @@ ], "license": "MIT" }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2884,6 +2990,18 @@ "node": ">=0.8.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undici": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz", diff --git a/package.json b/package.json index 87bc149..7491707 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,11 @@ }, "dependencies": { "body-parser": "^2.2.0", + "call-of-duty-api": "^4.1.0", "csso": "^5.0.5", "express": "^4.21.2", + "express-rate-limit": "^7.5.0", + "express-session": "^1.18.1", "glob": "^11.0.1", "html-minifier": "^4.0.0", "pkg": "^5.8.1", diff --git a/src/css/styles.css b/src/css/styles.css index 7669571..153bdbc 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -228,6 +228,22 @@ button:hover { background-color: #d32f2f; } +/* Demo mode styles */ +.demo-mode { + background-color: #f0f0f0; + color: #888; + cursor: not-allowed; +} + +.demo-notice { + background-color: #f8f9fa; + border-left: 4px solid #007bff; + padding: 10px 15px; + margin-bottom: 20px; + border-radius: 4px; + color: #495057; +} + @keyframes octocat-wave { 0%, 100% { diff --git a/src/js/backend-min.js b/src/js/backend-min.js new file mode 100644 index 0000000..285e576 --- /dev/null +++ b/src/js/backend-min.js @@ -0,0 +1,381 @@ +const a0_0x3e5ceb = a0_0x1776; +function a0_0x424e() { + const _0x308173 = [ + '8HLbzTb', + 'number', + 'replace', + 'endTime', + 'timePlayed', + 'uiAPI', + 'GMT', + 'forEach', + '[CLIENT]\x20Request\x20data:\x20', + 'An\x20error\x20occurred', + '[CLIENT]\x20Response\x20received\x20at\x20', + '\x20Hours\x20', + 'includes', + '\x20at\x20', + 'getElementById', + 'dateAdded', + 'toISOString', + 'textContent', + 'Request\x20timed\x20out.\x20Please\x20try\x20again.', + 'string', + 'Fetch\x20error:', + 'length', + '828318JvPrOY', + 'time', + 'replaceKeysOption', + 'toString', + 'substring', + 'json', + 'An\x20error\x20occurred\x20while\x20fetching\x20data.', + 'keys', + 'entries', + '274851lFAePH', + 'displayError', + 'toUTCString', + 'checked', + '5mFBRcl', + '1809891DxJKoQ', + 'DOMContentLoaded', + '7128aSefdV', + 'Server\x20returned\x20non-JSON\x20response', + 'none', + 'log', + 'stringify', + 'currentData', + 'object', + 'displayResults', + 'appState', + 'headers', + 'null', + 'signal', + '\x20Days\x20', + 'error', + 'style', + 'test', + '\x20Minutes\x20', + 'utcStartSeconds', + 'tutorialDismissed', + 'isArray', + 'trimStart', + 'ssoToken', + 'querySelectorAll', + '70rCMOkH', + 'addEventListener', + '1218049tFLRcR', + '.tutorial', + 'repeat', + '16176Hmtcab', + 'message', + 'loading', + '[CLIENT]\x20Response\x20status:\x20', + 'map', + 'POST', + 'floor', + '70193EjRYyO', + 'name', + 'objTime', + 'display', + 'date', + 'get', + 'Error:\x20', + 'content-type', + 'application/json', + 'status', + 'UTC', + 'boolean', + '2031464mgplJK', + '8ohsaRV', + ]; + a0_0x424e = function () { + return _0x308173; + }; + return a0_0x424e(); +} +(function (_0x26b91f, _0x29f7ca) { + const _0x4906c0 = a0_0x1776, + _0x243b93 = _0x26b91f(); + while (!![]) { + try { + const _0x1e80bd = + (parseInt(_0x4906c0(0x11e)) / 0x1) * + (-parseInt(_0x4906c0(0x12b)) / 0x2) + + -parseInt(_0x4906c0(0x150)) / 0x3 + + parseInt(_0x4906c0(0x12a)) / 0x4 + + (-parseInt(_0x4906c0(0x14f)) / 0x5) * + (-parseInt(_0x4906c0(0x142)) / 0x6) + + (parseInt(_0x4906c0(0x114)) / 0x7) * + (parseInt(_0x4906c0(0x12c)) / 0x8) + + (parseInt(_0x4906c0(0x14b)) / 0x9) * + (-parseInt(_0x4906c0(0x112)) / 0xa) + + (-parseInt(_0x4906c0(0x152)) / 0xb) * + (-parseInt(_0x4906c0(0x117)) / 0xc); + if (_0x1e80bd === _0x29f7ca) break; + else _0x243b93['push'](_0x243b93['shift']()); + } catch (_0x14bf47) { + _0x243b93['push'](_0x243b93['shift']()); + } + } +})(a0_0x424e, 0x91684), + (window['backendAPI'] = { + fetchData: fetchData, + jsonToYAML: jsonToYAML, + formatDuration: formatDuration, + formatEpochTime: formatEpochTime, + processTimestamps: processTimestamps, + }), + (window['appState'] = { + currentData: null, + outputFormat: a0_0x3e5ceb(0x147), + tutorialDismissed: ![], + }), + document[a0_0x3e5ceb(0x113)](a0_0x3e5ceb(0x151), function () {}); +function jsonToYAML(_0x57e066) { + const _0x4e69bf = a0_0x3e5ceb, + _0x1e3ebf = 0x2; + function _0x58e6eb(_0x4d120a, _0x248fd6 = 0x0) { + const _0x3a284a = a0_0x1776, + _0x52d5f6 = '\x20'[_0x3a284a(0x116)](_0x248fd6); + if (_0x4d120a === null) return _0x3a284a(0x105); + if (_0x4d120a === undefined) return ''; + if (typeof _0x4d120a === _0x3a284a(0x13f)) { + if ( + /[:{}[\],&*#?|\-<>=!%@`]/[_0x3a284a(0x10a)](_0x4d120a) || + _0x4d120a === '' || + !isNaN(_0x4d120a) + ) + return '\x22' + _0x4d120a[_0x3a284a(0x12e)](/"/g, '\x5c\x22') + '\x22'; + return _0x4d120a; + } + if ( + typeof _0x4d120a === _0x3a284a(0x12d) || + typeof _0x4d120a === _0x3a284a(0x129) + ) + return _0x4d120a[_0x3a284a(0x145)](); + if (Array[_0x3a284a(0x10e)](_0x4d120a)) { + if (_0x4d120a[_0x3a284a(0x141)] === 0x0) return '[]'; + let _0x53132f = ''; + for (const _0x194c43 of _0x4d120a) { + _0x53132f += + '\x0a' + + _0x52d5f6 + + '-\x20' + + _0x58e6eb(_0x194c43, _0x248fd6 + _0x1e3ebf)[_0x3a284a(0x10f)](); + } + return _0x53132f; + } + if (typeof _0x4d120a === 'object') { + if (Object[_0x3a284a(0x149)](_0x4d120a)[_0x3a284a(0x141)] === 0x0) + return '{}'; + let _0x42a784 = ''; + for (const [_0x1a8665, _0x32613d] of Object[_0x3a284a(0x14a)]( + _0x4d120a + )) { + const _0x3a78cc = _0x58e6eb(_0x32613d, _0x248fd6 + _0x1e3ebf); + _0x3a78cc[_0x3a284a(0x138)]('\x0a') ? + (_0x42a784 += '\x0a' + _0x52d5f6 + _0x1a8665 + ':' + _0x3a78cc) + : (_0x42a784 += '\x0a' + _0x52d5f6 + _0x1a8665 + ':\x20' + _0x3a78cc); + } + return _0x42a784; + } + return String(_0x4d120a); + } + return _0x58e6eb(_0x57e066, 0x0)[_0x4e69bf(0x146)](0x1); +} +async function fetchData(_0x5d7e74, _0x3de528) { + const _0x5f0540 = a0_0x3e5ceb; + console['log']( + '[CLIENT]\x20Request\x20to\x20' + + _0x5d7e74 + + _0x5f0540(0x139) + + new Date()[_0x5f0540(0x13c)]() + ), + console['log']( + _0x5f0540(0x134) + + JSON[_0x5f0540(0x156)]({ + ..._0x3de528, + ssoToken: + _0x3de528[_0x5f0540(0x110)] ? + _0x3de528['ssoToken'][_0x5f0540(0x146)](0x0, 0x5) + '...' + : _0x5f0540(0x154), + }) + ); + const _0x4d0c9e = document['getElementById']('error'), + _0x4a22b8 = document['getElementById'](_0x5f0540(0x119)), + _0x2f0994 = document[_0x5f0540(0x13a)]('results'); + (_0x4d0c9e[_0x5f0540(0x13d)] = ''), + (_0x2f0994[_0x5f0540(0x109)][_0x5f0540(0x121)] = _0x5f0540(0x154)), + (_0x4a22b8[_0x5f0540(0x109)][_0x5f0540(0x121)] = 'block'); + !window[_0x5f0540(0x103)][_0x5f0540(0x10d)] && + ((window[_0x5f0540(0x103)][_0x5f0540(0x10d)] = !![]), + document[_0x5f0540(0x111)](_0x5f0540(0x115))[_0x5f0540(0x133)]( + (_0x520561) => { + const _0x511e86 = _0x5f0540; + _0x520561[_0x511e86(0x109)][_0x511e86(0x121)] = _0x511e86(0x154); + } + )); + if (!_0x3de528[_0x5f0540(0x110)]) { + window[_0x5f0540(0x131)][_0x5f0540(0x14c)]( + 'SSO\x20Token\x20is\x20required' + ), + (_0x4a22b8[_0x5f0540(0x109)][_0x5f0540(0x121)] = _0x5f0540(0x154)); + return; + } + try { + const _0x4c0dfe = new AbortController(), + _0x5e0646 = setTimeout(() => _0x4c0dfe['abort'](), 0x7530), + _0x7ef5a8 = await fetch(_0x5d7e74, { + method: _0x5f0540(0x11c), + headers: { 'Content-Type': _0x5f0540(0x126) }, + body: JSON[_0x5f0540(0x156)](_0x3de528), + signal: _0x4c0dfe[_0x5f0540(0x106)], + }); + clearTimeout(_0x5e0646); + const _0x64ad7 = _0x7ef5a8[_0x5f0540(0x104)][_0x5f0540(0x123)]( + _0x5f0540(0x125) + ); + if (!_0x64ad7 || !_0x64ad7[_0x5f0540(0x138)](_0x5f0540(0x126))) + throw new Error(_0x5f0540(0x153)); + const _0x572a77 = await _0x7ef5a8[_0x5f0540(0x147)](); + console['log'](_0x5f0540(0x136) + new Date()['toISOString']()), + console[_0x5f0540(0x155)](_0x5f0540(0x11a) + _0x7ef5a8['status']), + console[_0x5f0540(0x155)]( + '[CLIENT]\x20Response\x20size:\x20~' + + JSON[_0x5f0540(0x156)](_0x572a77)[_0x5f0540(0x141)] / 0x400 + + '\x20KB' + ); + if (!_0x7ef5a8['ok']) + throw new Error( + _0x572a77[_0x5f0540(0x108)] || + _0x572a77[_0x5f0540(0x118)] || + _0x5f0540(0x124) + _0x7ef5a8[_0x5f0540(0x127)] + ); + if (_0x572a77['error']) + window[_0x5f0540(0x131)][_0x5f0540(0x14c)](_0x572a77[_0x5f0540(0x108)]); + else + _0x572a77['status'] === _0x5f0540(0x108) ? + window[_0x5f0540(0x131)][_0x5f0540(0x14c)]( + _0x572a77['message'] || _0x5f0540(0x135) + ) + : ((window[_0x5f0540(0x103)][_0x5f0540(0x100)] = _0x572a77), + window[_0x5f0540(0x131)][_0x5f0540(0x102)](_0x572a77)); + } catch (_0x563951) { + _0x563951[_0x5f0540(0x11f)] === 'AbortError' ? + window[_0x5f0540(0x131)]['displayError'](_0x5f0540(0x13e)) + : (window['uiAPI'][_0x5f0540(0x14c)]( + _0x5f0540(0x124) + (_0x563951[_0x5f0540(0x118)] || _0x5f0540(0x148)) + ), + console['error'](_0x5f0540(0x140), _0x563951)); + } finally { + _0x4a22b8[_0x5f0540(0x109)][_0x5f0540(0x121)] = 'none'; + } +} +function formatDuration(_0x58e5da) { + const _0x547bb4 = a0_0x3e5ceb; + if (!_0x58e5da || isNaN(_0x58e5da)) return _0x58e5da; + const _0x2efa31 = parseFloat(_0x58e5da), + _0x5a8e77 = Math[_0x547bb4(0x11d)](_0x2efa31 / 0x15180), + _0x31860f = Math[_0x547bb4(0x11d)]((_0x2efa31 % 0x15180) / 0xe10), + _0x40f2d9 = Math[_0x547bb4(0x11d)]((_0x2efa31 % 0xe10) / 0x3c), + _0x364d47 = Math[_0x547bb4(0x11d)](_0x2efa31 % 0x3c); + return ( + _0x5a8e77 + + _0x547bb4(0x107) + + _0x31860f + + _0x547bb4(0x137) + + _0x40f2d9 + + _0x547bb4(0x10b) + + _0x364d47 + + '\x20Seconds' + ); +} +function formatEpochTime(_0x2ad881, _0x376584) { + const _0x564df4 = a0_0x3e5ceb; + if (!_0x2ad881) return _0x2ad881; + const _0x2eb117 = parseInt(_0x2ad881); + if (isNaN(_0x2eb117)) return _0x2ad881; + const _0x403f03 = + _0x2eb117['toString']()['length'] <= 0xa ? _0x2eb117 * 0x3e8 : _0x2eb117; + let _0x56476d = 0x0; + if (_0x376584 !== _0x564df4(0x128)) { + const _0x34e2a2 = _0x376584['match'](/GMT([+-])(\d+)(?::(\d+))?/); + if (_0x34e2a2) { + const _0x27099d = _0x34e2a2[0x1] === '+' ? 0x1 : -0x1, + _0x16eec3 = parseInt(_0x34e2a2[0x2]), + _0x3bff6c = _0x34e2a2[0x3] ? parseInt(_0x34e2a2[0x3]) : 0x0; + _0x56476d = _0x27099d * (_0x16eec3 * 0x3c + _0x3bff6c) * 0x3c * 0x3e8; + } + } + const _0x56f46a = new Date(_0x403f03 + _0x56476d); + return _0x56f46a[_0x564df4(0x14d)]()[_0x564df4(0x12e)]( + _0x564df4(0x132), + _0x376584 + ); +} +function a0_0x1776(_0x51f135, _0x4e9ae9) { + const _0x424e92 = a0_0x424e(); + return ( + (a0_0x1776 = function (_0x177606, _0x203857) { + _0x177606 = _0x177606 - 0x100; + let _0x3fd5d4 = _0x424e92[_0x177606]; + return _0x3fd5d4; + }), + a0_0x1776(_0x51f135, _0x4e9ae9) + ); +} +function processTimestamps( + _0x13d227, + _0x1f087c, + _0x345fbc = [ + a0_0x3e5ceb(0x122), + a0_0x3e5ceb(0x13b), + a0_0x3e5ceb(0x10c), + 'utcEndSeconds', + 'timestamp', + 'startTime', + a0_0x3e5ceb(0x12f), + ], + _0x3c9b02 = [ + a0_0x3e5ceb(0x143), + 'timePlayedTotal', + a0_0x3e5ceb(0x130), + 'avgLifeTime', + 'duration', + a0_0x3e5ceb(0x120), + ] +) { + const _0x4c2013 = a0_0x3e5ceb; + if (!_0x13d227 || typeof _0x13d227 !== _0x4c2013(0x101)) return _0x13d227; + if (Array[_0x4c2013(0x10e)](_0x13d227)) + return _0x13d227[_0x4c2013(0x11b)]((_0x2088ab) => + processTimestamps(_0x2088ab, _0x1f087c, _0x345fbc, _0x3c9b02) + ); + const _0x134b47 = {}; + for (const [_0x1a08ca, _0x13b85e] of Object[_0x4c2013(0x14a)](_0x13d227)) { + if ( + _0x345fbc[_0x4c2013(0x138)](_0x1a08ca) && + typeof _0x13b85e === _0x4c2013(0x12d) + ) + _0x134b47[_0x1a08ca] = formatEpochTime(_0x13b85e, _0x1f087c); + else { + if ( + _0x3c9b02[_0x4c2013(0x138)](_0x1a08ca) && + typeof _0x13b85e === 'number' && + document[_0x4c2013(0x13a)](_0x4c2013(0x144))[_0x4c2013(0x14e)] + ) + _0x134b47[_0x1a08ca] = formatDuration(_0x13b85e); + else + typeof _0x13b85e === _0x4c2013(0x101) && _0x13b85e !== null ? + (_0x134b47[_0x1a08ca] = processTimestamps( + _0x13b85e, + _0x1f087c, + _0x345fbc, + _0x3c9b02 + )) + : (_0x134b47[_0x1a08ca] = _0x13b85e); + } + } + return _0x134b47; +} diff --git a/src/js/backend.js b/src/js/backend.js index fc66c6a..cef4b08 100644 --- a/src/js/backend.js +++ b/src/js/backend.js @@ -107,11 +107,11 @@ async function fetchData(endpoint, requestData) { } // Validate request data - if (!requestData.ssoToken) { - window.uiAPI.displayError('SSO Token is required'); - loadingElement.style.display = 'none'; - return; - } + // if (!requestData.ssoToken) { + // window.uiAPI.displayError('SSO Token is required'); + // loadingElement.style.display = 'none'; + // return; + // } try { // Set up the request with a timeout diff --git a/src/js/frontend.js b/src/js/frontend.js index fe32726..bc905be 100644 --- a/src/js/frontend.js +++ b/src/js/frontend.js @@ -67,8 +67,93 @@ document.addEventListener('DOMContentLoaded', function () { setupTimeOptions(); addSyncListeners(); initializeSessionTracking(); + checkDemoMode(); + // Call initially and then set up a refresh interval + updateRequestLimitInfo(); }); +setInterval(updateRequestLimitInfo, 60000); // Update every minute + +// Function to check if we're in demo mode +function checkDemoMode() { + console.log('Checking for demo mode...'); + fetch('/health') + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error ${response.status}`); + } + return response.json(); + }) + .then((data) => { + console.log('Demo mode status:', data.demoMode); + if (data.demoMode) { + enableDemoMode(); + } + }) + .catch((error) => { + console.error('Error checking demo mode:', error); + }); +} + +// Function to enable demo mode UI +function enableDemoMode() { + console.log('Enabling demo mode UI...'); + + // Get the SSO token input field + const ssoTokenInput = document.getElementById('ssoToken'); + if (ssoTokenInput) { + // Disable the input + ssoTokenInput.disabled = true; + // Add a placeholder indicating demo mode + ssoTokenInput.placeholder = 'Demo Mode - No SSO Token Required'; + // Gray it out with a CSS class + ssoTokenInput.classList.add('demo-mode'); + + // Add a demo mode indicator above the form + const formContainer = document.querySelector('.form-container'); + if (formContainer) { + // Check if notice already exists to avoid duplicates + if (!document.querySelector('.demo-notice')) { + const demoNotice = document.createElement('div'); + demoNotice.className = 'demo-notice'; + demoNotice.innerHTML = + '<strong>🎮 Demo Mode Active</strong> - Using a pre-configured token. No need to enter your own SSO token.'; + formContainer.insertBefore(demoNotice, formContainer.firstChild); + } + } + } + + // Ensure this is applied to window.appState + window.appState = window.appState || {}; + window.appState.demoMode = true; +} + +function updateRequestLimitInfo() { + fetch('/health') + .then((response) => response.json()) + .then((data) => { + const limitInfoElement = document.querySelector('.request-limit-info'); + if (data.requestsRemaining !== null) { + const message = + data.requestsRemaining > 0 ? + `Demo mode requests remaining: <strong>${data.requestsRemaining}</strong> (Resets hourly)` + : `Request limit reached. Please try again later or use your own SSO token.`; + + if (limitInfoElement) { + limitInfoElement.innerHTML = `<small>${message}</small>`; + } else { + const formContainer = document.querySelector('.form-container'); + if (formContainer) { + const limitInfo = document.createElement('div'); + limitInfo.className = 'request-limit-info'; + limitInfo.innerHTML = `<small>${message}</small>`; + formContainer.appendChild(limitInfo); + } + } + } + }); +} + // Tab switching logic function initTabSwitching() { document.querySelectorAll('.tab').forEach((tab) => { diff --git a/src/js/index.d.ts b/src/js/index.d.ts index 72b99a0..e0a2035 100644 --- a/src/js/index.d.ts +++ b/src/js/index.d.ts @@ -68,6 +68,11 @@ declare class MW { matchInfo: (matchId: string, platform: platforms) => Promise<unknown>; seasonloot: (gamertag: string, platform: platforms) => Promise<unknown>; mapList: (platform: platforms) => Promise<unknown>; + communityMapDataForMapMode: ( + mapName: string, + gameMode: string, + platform: platforms + ) => Promise<unknown>; } declare class MW2 { fullData: (unoId: string) => Promise<unknown>; diff --git a/src/js/index.js b/src/js/index.js index 96edf38..2e94853 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -55,6 +55,7 @@ let basePostHeaders = { 'user-agent': userAgent, }; let baseUrl = 'https://profile.callofduty.com'; +let custombaseUrl = 'https://www.callofduty.com'; let apiPath = '/api/papi-client'; let baseTelescopeUrl = 'https://telescope.callofduty.com'; let apiTelescopePath = '/api/ts-api'; @@ -166,6 +167,32 @@ const sendRequest = (url) => throw exception; } }); +const sendRequestCustom = (url) => + tslib_1.__awaiter(void 0, void 0, void 0, function* () { + try { + if (!loggedIn) throw new Error('Not Logged In.'); + let requestUrl = `${custombaseUrl}${apiPath}${url}`; + if (debugMode) console.log(`[DEBUG]`, `Request Uri: ${requestUrl}`); + if (debugMode) console.time('Round Trip'); + const { body, statusCode } = yield (0, undici_1.request)(requestUrl, { + headers: baseHeaders, + }); + if (debugMode) console.timeEnd('Round Trip'); + if (statusCode >= 500) + throw new Error( + `Received status code: '${statusCode}'. Route may be down or not exist.` + ); + let response = yield body.json(); + if (debugMode) + console.log( + `[DEBUG]`, + `Body Size: ${JSON.stringify(response).length} bytes.` + ); + return response; + } catch (exception) { + throw exception; + } + }); const sendPostRequest = (url, data) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { try { @@ -314,6 +341,8 @@ class Endpoints { `/codfriends/v1/${action}/${this.platform}/${this.lookupType}/${this.gamertag}`; this.search = () => `/crm/cod/v2/platform/${this.platform}/username/${this.gamertag}/search`; + this.communityMapDataForMapMode = (mapName, gameMode) => + `/ce/v1/title/${this.game}/platform/${this.platform}/gameType/${this.mode}/map/${mapName}/mode/${gameMode}/communityMapData`; this.game = game; this.gamertag = gamertag; this.platform = platform; @@ -607,6 +636,26 @@ class MW { return yield sendRequest(endpoint.mapList()); }); }; + this.communityMapDataForMapMode = (mapName, gameMode, platform) => { + var gamertag, platform, lookupType; + return tslib_1.__awaiter(this, void 0, void 0, function* () { + ({ + gamertag, + _platform: platform, + lookupType, + } = mapGamertagToPlatform('', platform)); + const endpoint = new Endpoints( + games.ModernWarfare, + gamertag, + platform, + modes.Multiplayer, + lookupType + ); + return yield sendRequestCustom( + endpoint.communityMapDataForMapMode(mapName, gameMode) + ); + }); + }; } } class MW2 { diff --git a/src/js/index.ts b/src/js/index.ts index d369c40..cbc41ed 100644 --- a/src/js/index.ts +++ b/src/js/index.ts @@ -46,6 +46,7 @@ let basePostHeaders: CustomHeaders = { }; let baseUrl: string = 'https://profile.callofduty.com'; +let custombaseUrl = 'https://www.callofduty.com'; let apiPath: string = '/api/papi-client'; let baseTelescopeUrl: string = 'https://telescope.callofduty.com'; let apiTelescopePath: string = '/api/ts-api'; @@ -183,6 +184,39 @@ const sendRequest = async (url: string) => { } }; +const sendRequestCustom = async (url: string) => { + try { + if (!loggedIn) throw new Error('Not Logged In.'); + let requestUrl = `${custombaseUrl}${apiPath}${url}`; + + if (debugMode) console.log(`[DEBUG]`, `Request Uri: ${requestUrl}`); + if (debugMode) console.time('Round Trip'); + + const { body, statusCode } = await request(requestUrl, { + headers: baseHeaders, + }); + + if (debugMode) console.timeEnd('Round Trip'); + + if (statusCode >= 500) + throw new Error( + `Received status code: '${statusCode}'. Route may be down or not exist.` + ); + + let response = await body.json(); + + if (debugMode) + console.log( + `[DEBUG]`, + `Body Size: ${JSON.stringify(response).length} bytes.` + ); + + return response; + } catch (exception: unknown) { + throw exception; + } +}; + const sendPostRequest = async (url: string, data: string) => { try { if (!loggedIn) throw new Error('Not Logged In.'); @@ -367,6 +401,8 @@ class Endpoints { `/codfriends/v1/${action}/${this.platform}/${this.lookupType}/${this.gamertag}`; search = () => `/crm/cod/v2/platform/${this.platform}/username/${this.gamertag}/search`; + communityMapDataForMapMode = (mapName: string, gameMode: string) => + `/ce/v1/title/${this.game}/platform/${this.platform}/gameType/${this.mode}/map/${mapName}/mode/${gameMode}/communityMapData`; } class TelescopeEndpoints { @@ -646,6 +682,28 @@ class MW { }; } +communityMapDataForMapMode = async ( + mapName: string, + gameMode: string, + platform: platforms +) => { + var { + gamertag, + _platform: platform, + lookupType, + } = mapGamertagToPlatform('', platform); + const endpoint = new Endpoints( + games.ModernWarfare, + gamertag, + platform, + modes.Multiplayer, + lookupType + ); + return await sendRequestCustom( + endpoint.communityMapDataForMapMode(mapName, gameMode) + ); +}; + class MW2 { fullData = async (unoId: string) => { var { gamertag } = mapGamertagToPlatform(unoId, platforms.Uno, true); diff --git a/src/js/test.js b/src/js/test.js new file mode 100644 index 0000000..08c9606 --- /dev/null +++ b/src/js/test.js @@ -0,0 +1,41 @@ +import { login, ModernWarfare, platforms } from './index.js'; +import fs from 'fs'; + +// Login using the SSO token +const ssoToken = + 'MTk1NjgyNzA6MTc0NDQ4OTcxNDE4MDpiOGE2MDEwMzY1ZWQ5OTM0NGM3ZjA0MWQxMTFjMTExNA'; + +(async () => { + try { + await login(ssoToken); // Ensure login is successful before making requests + + // // Fetch player data + // let gamertag = 'Ahrimdon'; + // let platform = platforms.XBOX; // Use predefined constants for platforms + + // let data = await ModernWarfare.fullData(gamertag, platform); + // console.log('Fetched data:', data); + + // // Convert data to a JSON string with formatting + // const jsonData = JSON.stringify(data, null, 2); + + // // Write the JSON data to a file + // fs.writeFileSync('playerData.json', jsonData); + + // Fetch community map data for a specific map and mode + const mapData = await ModernWarfare.communityMapDataForMapMode( + 'mp_m_king', + 'gun', + platforms.XBOX + ); + console.log('Map data:', mapData); + + // Save to file if needed + fs.writeFileSync('mapData.json', JSON.stringify(mapData, null, 2)); + console.log('Map data has been saved to mapData.json'); + + // console.log('Player data has been saved to playerData.json'); + } catch (error) { + console.error('Error fetching data:', error); + } +})(); diff --git a/token.txt b/token.txt new file mode 100644 index 0000000..bd816fe --- /dev/null +++ b/token.txt @@ -0,0 +1 @@ +<YOUR_SSO_TOKEN_HERE> \ No newline at end of file