Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/HeyPuter/puter into main
Browse files Browse the repository at this point in the history
  • Loading branch information
jelveh committed Apr 23, 2024
2 parents 5538649 + 97ee951 commit 3be7af8
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 4 deletions.
3 changes: 3 additions & 0 deletions packages/backend/src/CoreModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
10 changes: 10 additions & 0 deletions packages/backend/src/routers/change_email.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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');
Expand Down
7 changes: 3 additions & 4 deletions packages/backend/src/routers/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/routers/passwd.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/routers/save_account.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/routers/send-confirm-email.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/routers/send-pass-recovery-email.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/routers/set-pass-using-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ?',
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/routers/signup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 3be7af8

Please sign in to comment.