mirror of
https://github.com/HeyPuter/puter
synced 2024-11-15 06:15:47 +00:00
Add ip rate limiting
This commit is contained in:
parent
65a73b5b45
commit
44aac16991
@ -192,6 +192,9 @@ const install = async ({ services, app }) => {
|
||||
|
||||
const { SessionService } = require('./services/SessionService');
|
||||
services.registerService('session', SessionService);
|
||||
|
||||
const { EdgeRateLimitService } = require('./services/abuse-prevention/EdgeRateLimitService');
|
||||
services.registerService('edge-rate-limit', EdgeRateLimitService);
|
||||
}
|
||||
|
||||
const install_legacy = async ({ services }) => {
|
||||
|
@ -51,6 +51,11 @@ const CHANGE_EMAIL_START = eggspress('/change_email/start', {
|
||||
key: 'new_email', expected: 'a valid email address' });
|
||||
}
|
||||
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('change-email-start') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
}
|
||||
|
||||
// check if email is already in use
|
||||
const db = req.services.get('database').get(DB_WRITE, 'auth');
|
||||
const rows = await db.read(
|
||||
@ -93,6 +98,11 @@ const CHANGE_EMAIL_CONFIRM = eggspress('/change_email/confirm', {
|
||||
throw APIError.create('field_missing', null, { key: 'token' });
|
||||
}
|
||||
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('change-email-confirm') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
}
|
||||
|
||||
const { token, user_id } = jwt.verify(jwt_token, config.jwt_secret);
|
||||
|
||||
const db = req.services.get('database').get(DB_WRITE, 'auth');
|
||||
|
@ -60,11 +60,10 @@ router.post('/login', express.json(), body_parser_error_handler, async (req, res
|
||||
else if(req.body.email && !validator.isEmail(req.body.email))
|
||||
return res.status(400).send('Invalid email.')
|
||||
|
||||
// Increment & check rate limit
|
||||
if(kv.incr(`login|${req.ip}|${req.body.email ?? req.body.username}`) > 10)
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('login') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
// Set expiry for rate limit
|
||||
kv.expire(`login|${req.ip}|${req.body.email ?? req.body.username}`, 60*10, 'NX')
|
||||
}
|
||||
|
||||
try{
|
||||
let user;
|
||||
|
@ -45,6 +45,11 @@ router.post('/passwd', auth, express.json(), async (req, res, next)=>{
|
||||
else if (typeof req.body.new_pass !== 'string')
|
||||
return res.status(400).send('new_pass must be a string.')
|
||||
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('passwd') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
}
|
||||
|
||||
try{
|
||||
// check old_pass
|
||||
const isMatch = await bcrypt.compare(req.body.old_pass, req.user.password)
|
||||
|
@ -70,6 +70,11 @@ router.post('/save_account', auth, express.json(), async (req, res, next)=>{
|
||||
else if(req.body.password.length < config.min_pass_length)
|
||||
return res.status(400).send(`Password must be at least ${config.min_pass_length} characters long.`)
|
||||
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('save-account') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
}
|
||||
|
||||
// duplicate username check, do this only if user has supplied a new username
|
||||
if(req.body.username !== req.user.username && await username_exists(req.body.username))
|
||||
return res.status(400).send('This username already exists in our database. Please use another one.');
|
||||
|
@ -27,6 +27,11 @@ const { DB_WRITE } = require('../services/database/consts.js');
|
||||
// POST /send-confirm-email
|
||||
// -----------------------------------------------------------------------//
|
||||
router.post('/send-confirm-email', auth, express.json(), async (req, res, next)=>{
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('send-confirm-email') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
}
|
||||
|
||||
// check subdomain
|
||||
if(require('../helpers').subdomain(req) !== 'api')
|
||||
next();
|
||||
|
@ -51,6 +51,12 @@ router.post('/send-pass-recovery-email', express.json(), body_parser_error_handl
|
||||
else if(req.body.email && !validator.isEmail(req.body.email))
|
||||
return res.status(400).send('Invalid email.')
|
||||
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('send-pass-recovery-email') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
}
|
||||
|
||||
|
||||
try{
|
||||
let user;
|
||||
// see if username exists
|
||||
|
@ -52,6 +52,11 @@ router.post('/set-pass-using-token', express.json(), async (req, res, next)=>{
|
||||
else if(req.body.password.length < config.min_pass_length)
|
||||
return res.status(400).send(`Password must be at least ${config.min_pass_length} characters long.`)
|
||||
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('set-pass-using-token') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
}
|
||||
|
||||
try{
|
||||
const info = await db.write(
|
||||
'UPDATE user SET password=?, pass_recovery_token=NULL WHERE `uuid` = ? AND pass_recovery_token = ?',
|
||||
|
@ -44,6 +44,11 @@ module.exports = eggspress(['/signup'], {
|
||||
if(require('../helpers').subdomain(req) !== 'api' && require('../helpers').subdomain(req) !== '')
|
||||
next();
|
||||
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('signup') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
}
|
||||
|
||||
// modules
|
||||
const db = req.services.get('database').get(DB_WRITE, 'auth');
|
||||
const bcrypt = require('bcrypt')
|
||||
|
@ -0,0 +1,95 @@
|
||||
const { Context } = require("../../util/context");
|
||||
const { asyncSafeSetInterval } = require("../../util/promise");
|
||||
|
||||
const { MINUTE, HOUR } = require('../../util/time.js');
|
||||
const BaseService = require("../BaseService");
|
||||
|
||||
class EdgeRateLimitService extends BaseService {
|
||||
_construct () {
|
||||
this.scopes = {
|
||||
['login']: {
|
||||
limit: 3,
|
||||
window: 15 * MINUTE,
|
||||
},
|
||||
['signup']: {
|
||||
limit: 10,
|
||||
window: 15 * MINUTE,
|
||||
},
|
||||
['send-confirm-email']: {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
['send-pass-recovery-email']: {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
['set-pass-using-token']: {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
['save-account']: {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
['change-email-start']: {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
['change-email-confirm']: {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
['passwd']: {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
};
|
||||
this.requests = new Map();
|
||||
}
|
||||
|
||||
async _init () {
|
||||
asyncSafeSetInterval(() => this.cleanup(), 5 * MINUTE);
|
||||
}
|
||||
|
||||
check (scope) {
|
||||
const { window, limit } = this.scopes[scope];
|
||||
|
||||
const requester = Context.get('requester');
|
||||
const rl_identifier = requester.rl_identifier;
|
||||
const key = `${scope}:${rl_identifier}`;
|
||||
const now = Date.now();
|
||||
const windowStart = now - window;
|
||||
|
||||
if (!this.requests.has(key)) {
|
||||
this.requests.set(key, []);
|
||||
}
|
||||
|
||||
// Access the timestamps of past requests for this scope and IP
|
||||
const timestamps = this.requests.get(key);
|
||||
|
||||
// Remove timestamps that are outside the current window
|
||||
while (timestamps.length > 0 && timestamps[0] < windowStart) {
|
||||
timestamps.shift();
|
||||
}
|
||||
|
||||
// Check if the current request exceeds the rate limit
|
||||
if (timestamps.length >= limit) {
|
||||
return false;
|
||||
} else {
|
||||
// Add current timestamp and allow the request
|
||||
timestamps.push(now);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.log.tick('edge rate-limit cleanup task');
|
||||
for (const [key, timestamps] of this.requests.entries()) {
|
||||
if (timestamps.length === 0) {
|
||||
this.requests.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { EdgeRateLimitService };
|
@ -68,6 +68,10 @@ class Requester {
|
||||
return puter_origins.includes(this.origin);
|
||||
}
|
||||
|
||||
get rl_identifier () {
|
||||
return this.ip_forwarded || this.ip;
|
||||
}
|
||||
|
||||
serialize () {
|
||||
return {
|
||||
ua: this.ua,
|
||||
|
Loading…
Reference in New Issue
Block a user