diff --git a/package.json b/package.json index ed4f587..9a388c4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dotenv": "^16.0.3", "express": "^4.17.1", "express-http-context2": "^1.0.0", + "express-rate-limit": "^7.0.1", "express-validator": "^6.13.0", "got": "^11.8.2", "hash-wasm": "^4.9.0", diff --git a/src/routes/profile.js b/src/routes/profile.js index 9211bc5..9e15a1a 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -29,18 +29,42 @@ more information on this, and how to apply and follow the GNU AGPL, see { +let profileRateLimiter = (req, res, next) => { + next() +} + +if (process.env.ENABLE_EXPERIMENTAL_RATE_LIMITER) { + profileRateLimiter = rateLimit({ + windowMs: 1000, + limit: 3, + standardHeaders: 'draft-7', + legacyHeaders: false, + handler: (req, res, next, options) => { + logger.debug('Rate-limiting a profile request', + { component: 'profile_rate_limiter', action: 'block' }) + + res.status(options.statusCode).render('429', { meta: getMetaFromReq(req) }) + } + }) + + logger.debug('Starting the profile request rate limiter', + { component: 'profile_rate_limiter', action: 'start' }) +} + +router.get('/sig', profileRateLimiter, (req, res) => { res.render('profile', { isSignature: true, signature: null, meta: getMetaFromReq(req) }) }) -router.post('/sig', bodyParser, async (req, res) => { +router.post('/sig', profileRateLimiter, bodyParser, async (req, res) => { const data = await generateSignatureProfile(req.body.signature) const title = utils.generatePageTitle('profile', data) res.set('ariadne-identity-proof', data.identifier) @@ -55,7 +79,7 @@ router.post('/sig', bodyParser, async (req, res) => { }) }) -router.get('/wkd/:id', async (req, res) => { +router.get('/wkd/:id', profileRateLimiter, async (req, res) => { const data = await generateWKDProfile(req.params.id) const title = utils.generatePageTitle('profile', data) res.set('ariadne-identity-proof', data.identifier) @@ -68,7 +92,7 @@ router.get('/wkd/:id', async (req, res) => { }) }) -router.get('/hkp/:id', async (req, res) => { +router.get('/hkp/:id', profileRateLimiter, async (req, res) => { const data = await generateHKPProfile(req.params.id) const title = utils.generatePageTitle('profile', data) res.set('ariadne-identity-proof', data.identifier) @@ -81,7 +105,7 @@ router.get('/hkp/:id', async (req, res) => { }) }) -router.get('/hkp/:server/:id', async (req, res) => { +router.get('/hkp/:server/:id', profileRateLimiter, async (req, res) => { const data = await generateHKPProfile(req.params.id, req.params.server) const title = utils.generatePageTitle('profile', data) res.set('ariadne-identity-proof', data.identifier) @@ -94,7 +118,7 @@ router.get('/hkp/:server/:id', async (req, res) => { }) }) -router.get('/keybase/:username/:fingerprint', async (req, res) => { +router.get('/keybase/:username/:fingerprint', profileRateLimiter, async (req, res) => { const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint) const title = utils.generatePageTitle('profile', data) res.set('ariadne-identity-proof', data.identifier) @@ -107,7 +131,7 @@ router.get('/keybase/:username/:fingerprint', async (req, res) => { }) }) -router.get('/:id', async (req, res) => { +router.get('/:id', profileRateLimiter, async (req, res) => { const data = await generateAutoProfile(req.params.id) const title = utils.generatePageTitle('profile', data) res.set('ariadne-identity-proof', data.identifier) diff --git a/template.env b/template.env index 5f05cf4..d4a974f 100644 --- a/template.env +++ b/template.env @@ -36,4 +36,8 @@ # Enable caching of keys (experimental) # Opt-in; to disable, omit the environment variable -#ENABLE_EXPERIMENTAL_CACHE= +#ENABLE_EXPERIMENTAL_CACHE=true + +# Enable profile request rate limiting (experimental) +# Opt-in; to disable, omit the environment variable +#ENABLE_EXPERIMENTAL_RATE_LIMITER=true diff --git a/views/429.pug b/views/429.pug new file mode 100644 index 0000000..48bb4cd --- /dev/null +++ b/views/429.pug @@ -0,0 +1,8 @@ +extends templates/base.pug + +block content + h1 429 TOO MANY REQUESTS + p + | Too many requests from this IP, please try again later. + br + | Limit: 3 profile requests per second. diff --git a/yarn.lock b/yarn.lock index a4dfb29..806c582 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2269,6 +2269,11 @@ express-http-context2@^1.0.0: resolved "https://registry.yarnpkg.com/express-http-context2/-/express-http-context2-1.0.0.tgz#58cd9fb0d233739e0dcd7aabb766d1dc74522d77" integrity sha512-xdukoNNpWcuMn5ZJcjDe/tA+2A96rQ1MyAB/oWUU7qP15Tkz3txQyFsw/QG8YgRzTJ1sNAA8Bdq0o5b/1Y4zLA== +express-rate-limit@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.0.1.tgz#933af24166990ea4fc8004335e6cd6c86fd31562" + integrity sha512-oTIPm094gh8c7nbShl4TNLqnayzOcbDGY7dCRnFqUAvptyb0pp5231LaH34JtvVEbZlOJMiixikU5AVK8VN3FA== + express-validator@^6.10.0, express-validator@^6.13.0: version "6.15.0" resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.15.0.tgz#5e4601428960b0d66f5f4ae09cb32ed2077374a4"