Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementa Rate Limit na API: /api/v1/sessions #635

Merged
merged 7 commits into from
Aug 16, 2022
Merged
9 changes: 6 additions & 3 deletions errors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ export class ValidationError extends BaseError {
}

export class UnauthorizedError extends BaseError {
constructor({ message, action, stack, errorLocationCode }) {
constructor({ message, action, requestId, stack, errorLocationCode }) {
super({
message: message || 'Usuário não autenticado.',
action: action || 'Verifique se você está autenticado com uma sessão ativa e tente novamente.',
requestId: requestId,
statusCode: 401,
stack: stack,
errorLocationCode: errorLocationCode,
Expand All @@ -101,10 +102,11 @@ export class UnauthorizedError extends BaseError {
}

export class ForbiddenError extends BaseError {
constructor({ message, action, stack, errorLocationCode }) {
constructor({ message, action, requestId, stack, errorLocationCode }) {
super({
message: message || 'Você não possui permissão para executar esta ação.',
action: action || 'Verifique se você possui permissão para executar esta ação.',
requestId: requestId,
statusCode: 403,
stack: stack,
errorLocationCode: errorLocationCode,
Expand All @@ -113,11 +115,12 @@ export class ForbiddenError extends BaseError {
}

export class TooManyRequestsError extends BaseError {
constructor({ message, action, stack, errorLocationCode }) {
constructor({ message, action, context, stack, errorLocationCode }) {
super({
message: message || 'Você realizou muitas requisições recentemente.',
action: action || 'Tente novamente mais tarde ou contate o suporte caso acredite que isso seja um erro.',
statusCode: 429,
context: context,
stack: stack,
errorLocationCode: errorLocationCode,
});
Expand Down
4 changes: 2 additions & 2 deletions infra/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async function query(query, options = {}) {
async function tryToGetNewClientFromPool() {
const clientFromPool = await retry(newClientFromPool, {
retries: 50,
minTimeout: 0,
minTimeout: 1000,
factor: 2,
});

Expand All @@ -76,7 +76,7 @@ async function tryToGetNewClientFromPool() {
async function checkForTooManyConnections(client) {
const currentTime = new Date().getTime();
const openedConnectionsMaxAge = 10000;
const maxConnectionsTolerance = 0.9;
const maxConnectionsTolerance = 0.7;

if (cache.maxConnections === null || cache.reservedConnections === null) {
const [maxConnections, reservedConnections] = await getConnectionLimits();
Expand Down
91 changes: 91 additions & 0 deletions infra/rate-limit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { ServiceError } from 'errors/index.js';

async function check(request) {
if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) {
if (['test', 'development'].includes(process.env.NODE_ENV) || process.env.CI || process.env.GITHUB_ACTIONS) {
return { success: true };
}

throw new ServiceError({
message: 'Variáveis de ambiente UPSTASH_REDIS_REST_URL ou UPSTASH_REDIS_REST_TOKEN não encontradas.',
action:
'Configure as variáveis de ambiente UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN para habilitar o serviço de rate-limiting.',
context: {
upstashRedisRestUrl: process.env.UPSTASH_REDIS_REST_URL ? true : false,
upstashRedisRestToken: process.env.UPSTASH_REDIS_REST_TOKEN ? true : false,
},
errorLocationCode: 'MIDDLEWARE:RATE_LIMIT:CHECK:ENV_MISSING',
});
}

const ip = getIP(request);
const method = request.method;
const path = request.nextUrl.pathname;
const generalIdentifier = `${ip}`;
const specificIdentifier = `${ip}:${method}:${path}`;
const limits = getLimits();

try {
if (method === 'POST' && path === '/api/v1/sessions') {
const rateLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(limits.postSessions.requests, limits.postSessions.window),
});

return await rateLimit.limit(specificIdentifier);
}

const generalRateLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(limits.general.requests, limits.general.window),
});

return await generalRateLimit.limit(generalIdentifier);
} catch (error) {
throw new ServiceError({
message: error.message,
action: 'Verifique se o serviço Upstash está disponível.',
stack: error.stack,
context: {
ip,
method,
path,
},
errorLocationCode: 'MIDDLEWARE:RATE_LIMIT:CHECK',
});
}
}

function getIP(request) {
const xff = request instanceof Request ? request.headers.get('x-forwarded-for') : request.headers['x-forwarded-for'];

return xff ? (Array.isArray(xff) ? xff[0] : xff.split(',')[0]) : '127.0.0.1';
}

function getLimits() {
const defaultLimits = {
general: {
requests: 1000,
window: '5 m',
},
postSessions: {
requests: 50,
window: '30 m',
},
};

const limits = process.env.RATE_LIMITS ? JSON.parse(process.env.RATE_LIMITS) : defaultLimits;

// `process.env.RATE_LIMITS` can exist without all the keys
// so we must fill them with the default values.
limits.general ? limits.general : defaultLimits.general;
limits.postSessions ? limits.postSessions : defaultLimits.postSessions;

return limits;
}

export default Object.freeze({
check,
});
30 changes: 30 additions & 0 deletions middleware.public.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import rateLimit from 'infra/rate-limit.js';
import snakeize from 'snakeize';

export const config = {
matcher: ['/api/v1/sessions'],
};

export async function middleware(request) {
const url = request.nextUrl;

try {
const rateLimitResult = await rateLimit.check(request);

if (!rateLimitResult.success && url.pathname === '/api/v1/sessions') {
url.pathname = '/api/v1/_responses/rate-limit-reached-sessions'; // Fake response.
return NextResponse.rewrite(url);
}

if (!rateLimitResult.success) {
url.pathname = '/api/v1/_responses/rate-limit-reached';
return NextResponse.rewrite(url);
}

return NextResponse.next();
} catch (error) {
console.error(snakeize(error));
return NextResponse.next();
}
}
Loading