refactor: шаг 2 — лимитеры вынесены в src/middleware/rateLimit.js
This commit is contained in:
@@ -2,7 +2,8 @@ const express = require('express');
|
|||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
const { rateLimit } = require('express-rate-limit');
|
// rateLimit.js: mutationLimiter (POST-роуты) и exportLimiter (GET /export)
|
||||||
|
const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit');
|
||||||
const { doubleCsrf } = require('csrf-csrf');
|
const { doubleCsrf } = require('csrf-csrf');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const { checkConnection } = require('./src/db');
|
const { checkConnection } = require('./src/db');
|
||||||
@@ -23,26 +24,6 @@ app.set('views', path.join(__dirname, 'views'));
|
|||||||
// Referrer-Policy и пр. CSP отключён явно — EJS использует inline-стили/скрипты.
|
// Referrer-Policy и пр. CSP отключён явно — EJS использует inline-стили/скрипты.
|
||||||
app.use(helmet({ contentSecurityPolicy: false }));
|
app.use(helmet({ contentSecurityPolicy: false }));
|
||||||
|
|
||||||
// ── Rate limiting ──────────────────────────────────────────────────────────────
|
|
||||||
// Мутирующие POST-маршруты: не более 30 запросов за 1 минуту с одного IP.
|
|
||||||
// Это защищает от случайного flood и простых скриптовых атак.
|
|
||||||
const mutationLimiter = rateLimit({
|
|
||||||
windowMs: 60_000, // окно 1 минута
|
|
||||||
limit: 30, // макс 30 POST-запросов
|
|
||||||
standardHeaders: 'draft-7', // Retry-After в заголовках
|
|
||||||
legacyHeaders: false,
|
|
||||||
message: { error: 'Слишком много запросов. Подождите минуту.' },
|
|
||||||
});
|
|
||||||
|
|
||||||
// /export доступен публично — ограничиваем отдельно, мягче (по IP)
|
|
||||||
const exportLimiter = rateLimit({
|
|
||||||
windowMs: 60_000,
|
|
||||||
limit: 20,
|
|
||||||
standardHeaders: 'draft-7',
|
|
||||||
legacyHeaders: false,
|
|
||||||
message: 'Слишком много запросов к /export.',
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
app.use(express.urlencoded({ extended: true, limit: '32kb' }));
|
app.use(express.urlencoded({ extended: true, limit: '32kb' }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* src/middleware/rateLimit.js — настройки ограничения частоты запросов.
|
||||||
|
*
|
||||||
|
* Зачем вынесено отдельно:
|
||||||
|
* - Лимиты можно менять в одном месте, не трогая роуты.
|
||||||
|
* - Легко подключать только нужный лимитер к конкретному роуту.
|
||||||
|
* - Упрощает тестирование — можно заменить на no-op лимитер в тестах.
|
||||||
|
*
|
||||||
|
* express-rate-limit считает запросы по IP (по умолчанию — req.ip).
|
||||||
|
* Если приложение стоит за reverse-proxy (nginx/traefik), нужно
|
||||||
|
* настроить app.set('trust proxy', 1) чтобы X-Forwarded-For работал корректно.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { rateLimit } = require('express-rate-limit');
|
||||||
|
|
||||||
|
// ── mutationLimiter — для мутирующих POST-запросов ────────────────────────────
|
||||||
|
// Применяется к: POST /add, /edit/:id, /delete/:id, /admin/limit/:id
|
||||||
|
//
|
||||||
|
// Лимит 30 запросов за минуту с одного IP — достаточно для ручного
|
||||||
|
// пользования и при этом блокирует простые скриптовые атаки.
|
||||||
|
//
|
||||||
|
// При превышении лимита — 429 Too Many Requests.
|
||||||
|
const mutationLimiter = rateLimit({
|
||||||
|
windowMs: 60_000, // скользящее окно: 1 минута
|
||||||
|
limit: 30, // не более 30 POST-запросов за окно
|
||||||
|
standardHeaders: 'draft-7', // добавляет заголовки RateLimit-* и Retry-After
|
||||||
|
legacyHeaders: false, // не добавлять устаревшие X-RateLimit-* заголовки
|
||||||
|
message: { error: 'Слишком много запросов. Подождите минуту.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── exportLimiter — для публичного GET /export ────────────────────────────────
|
||||||
|
// /export не требует авторизации (ТЗ) — поэтому ограничиваем жёстче.
|
||||||
|
// 20 запросов в минуту достаточно для автоматической синхронизации
|
||||||
|
// (nginx/firewall обычно опрашивают раз в несколько минут).
|
||||||
|
const exportLimiter = rateLimit({
|
||||||
|
windowMs: 60_000,
|
||||||
|
limit: 20,
|
||||||
|
standardHeaders: 'draft-7',
|
||||||
|
legacyHeaders: false,
|
||||||
|
message: 'Слишком много запросов к /export. Подождите минуту.',
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { mutationLimiter, exportLimiter };
|
||||||
Reference in New Issue
Block a user