feat(api): merkezi rate limiting sistemi ekle

Yeni rate-limiter middleware modülü oluşturuldu. loginLimiter (5 istek/dakika),
apiLimiter (30 istek/dakika) ve uploadLimiter (10 istek/dakika) tanımlandı.
Auth, loop, timer ve torrent rotalarına rate limiting uygulandı.
Torrent rotalarında SHA-1 hash validasyonu eklendi.
This commit is contained in:
2026-01-04 23:38:15 +03:00
parent b7a460596e
commit 377971411a
5 changed files with 68 additions and 24 deletions

View File

@@ -7,14 +7,22 @@ import { getArchiveStatus, setArchiveStatus } from "./torrent.archive";
import { nowIso } from "../utils/time";
import { appendAuditLog, logger } from "../utils/logger";
import { config } from "../config";
import { apiLimiter, uploadLimiter } from "../middleware/rate-limiter";
const router = Router();
const upload = multer({ dest: "/tmp" });
router.post("/select", async (req, res) => {
// qBittorrent hash'leri 40 karakter hexadecimal (SHA-1)
const VALID_HASH_REGEX = /^[a-f0-9]{40}$/i;
function isValidHash(hash: string): boolean {
return VALID_HASH_REGEX.test(hash);
}
router.post("/select", apiLimiter, async (req, res) => {
const { hash } = req.body ?? {};
if (!hash) {
return res.status(400).json({ error: "Missing hash" });
if (!hash || !isValidHash(hash)) {
return res.status(400).json({ error: "Geçersiz hash formatı" });
}
const existing = await getArchiveStatus(hash);
if (existing?.status === "READY") {
@@ -36,10 +44,10 @@ router.post("/select", async (req, res) => {
res.json({ ok: true, hash, archive: { status: "MISSING" } });
});
router.post("/archive/from-selected", async (req, res) => {
router.post("/archive/from-selected", apiLimiter, async (req, res) => {
const { hash } = req.body ?? {};
if (!hash) {
return res.status(400).json({ error: "Missing hash" });
if (!hash || !isValidHash(hash)) {
return res.status(400).json({ error: "Geçersiz hash formatı" });
}
const existing = await getArchiveStatus(hash);
if (existing?.status === "READY") {
@@ -59,10 +67,10 @@ router.post("/archive/from-selected", async (req, res) => {
return res.status(400).json({ error: "Magnet export disabled; upload .torrent manually." });
});
router.post("/archive/upload", upload.single("file"), async (req, res) => {
router.post("/archive/upload", uploadLimiter, upload.single("file"), async (req, res) => {
const { hash } = req.body ?? {};
if (!hash || !req.file) {
return res.status(400).json({ error: "Missing hash or file" });
if (!hash || !req.file || !isValidHash(hash)) {
return res.status(400).json({ error: "Geçersiz hash formatı veya dosya eksik" });
}
const inputHash = String(hash).toLowerCase();
const buffer = await fs.readFile(req.file.path);
@@ -111,9 +119,13 @@ router.post("/archive/upload", upload.single("file"), async (req, res) => {
});
router.get("/archive/status/:hash", async (req, res) => {
const status = await getArchiveStatus(req.params.hash);
const { hash } = req.params;
if (!isValidHash(hash)) {
return res.status(400).json({ error: "Geçersiz hash formatı" });
}
const status = await getArchiveStatus(hash);
if (!status) {
return res.json({ hash: req.params.hash, status: "MISSING" });
return res.json({ hash, status: "MISSING" });
}
return res.json(status);
});