-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
164 lines (137 loc) · 4.93 KB
/
middleware.ts
File metadata and controls
164 lines (137 loc) · 4.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import type { NextFetchEvent, NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getSecurityConfig, featuresConfig } from "@/lib/config";
interface AntiDDoSConfig {
/**
* Maximum number of requests allowed from a single client within the window.
*/
maxRequestsPerWindow: number;
/**
* Sliding window size in milliseconds.
*/
windowMs: number;
/**
* How long (in ms) to block a client after they exceed the limit. Defaults to windowMs.
*/
blockDurationMs?: number;
/**
* Optional URL or path to redirect suspected DDoS traffic to.
* If omitted, a 429 response is returned instead.
*/
redirectUrl?: string;
/**
* Function that derives a client key from the request (e.g. IP or header).
*/
keyGenerator?: (req: NextRequest) => string;
}
interface ClientState {
hits: number;
firstHitAt: number;
blockedUntil?: number;
}
// In-memory store keyed by client identifier. This is per-instance and not shared across servers.
const clients = new Map<string, ClientState>();
function getClientKey(req: NextRequest, keyGenerator?: (req: NextRequest) => string): string {
if (keyGenerator) return keyGenerator(req);
// Prefer x-forwarded-for (behind proxies/CDN), fall back to a generic "unknown" bucket.
const forwardedFor = req.headers.get("x-forwarded-for");
if (forwardedFor) {
// x-forwarded-for can be a comma-separated list; take the first IP.
return forwardedFor.split(",")[0]!.trim();
}
return "unknown";
}
function handleBlockedRequest(req: NextRequest, redirectUrl?: string) {
if (redirectUrl) {
const url = new URL(redirectUrl, req.url);
return NextResponse.redirect(url);
}
return new NextResponse("Too many requests", { status: 429 });
}
function createAntiDDoSMiddleware(config: AntiDDoSConfig) {
const {
maxRequestsPerWindow,
windowMs,
blockDurationMs = windowMs,
redirectUrl,
keyGenerator,
} = config;
if (maxRequestsPerWindow <= 0) {
throw new Error("maxRequestsPerWindow must be greater than 0");
}
if (windowMs <= 0) {
throw new Error("windowMs must be greater than 0");
}
return function antiDDoSMiddleware(req: NextRequest, _event?: NextFetchEvent) {
const key = getClientKey(req, keyGenerator);
const now = Date.now();
let state = clients.get(key);
if (!state) {
state = { hits: 0, firstHitAt: now };
clients.set(key, state);
}
// If client is currently blocked, short-circuit.
if (state.blockedUntil && now < state.blockedUntil) {
return handleBlockedRequest(req, redirectUrl);
}
// Reset the window if it has expired.
if (now - state.firstHitAt > windowMs) {
state.hits = 0;
state.firstHitAt = now;
state.blockedUntil = undefined;
}
state.hits += 1;
if (state.hits > maxRequestsPerWindow) {
// Mark client as blocked for the configured duration.
state.blockedUntil = now + blockDurationMs;
return handleBlockedRequest(req, redirectUrl);
}
// Allow the request to continue through the Next.js routing pipeline.
return NextResponse.next();
};
}
export function middleware(req: NextRequest, event: NextFetchEvent) {
// Check feature toggles for specific routes
const pathname = req.nextUrl.pathname;
// Feature toggle checks
if (pathname.startsWith('/leaderboard') && !featuresConfig.leaderboard) {
return NextResponse.redirect(new URL('/', req.url));
}
if (pathname.startsWith('/portfolios') && !featuresConfig.portfolios) {
return NextResponse.redirect(new URL('/', req.url));
}
if (pathname.startsWith('/offers') && !featuresConfig.offers) {
return NextResponse.redirect(new URL('/', req.url));
}
if (pathname.startsWith('/trade') && !featuresConfig.trading) {
return NextResponse.redirect(new URL('/', req.url));
}
if (pathname.startsWith('/company-values') && !featuresConfig.companyValues) {
return NextResponse.redirect(new URL('/', req.url));
}
if (pathname.startsWith('/moderator') && !featuresConfig.moderatorTools) {
return NextResponse.redirect(new URL('/', req.url));
}
// Apply DDoS protection if enabled (only on server-side)
if (typeof window === 'undefined') {
try {
const securityConfig = getSecurityConfig();
if (securityConfig.enableRateLimiting) {
const antiDdos = createAntiDDoSMiddleware({
maxRequestsPerWindow: securityConfig.maxRequestsPerMinute,
windowMs: 60_000, // 60 seconds
blockDurationMs: 5 * 60_000, // block for 5 minutes after abuse
redirectUrl: "/ddos-blocked", // redirect suspected DDoS traffic
});
return antiDdos(req, event);
}
} catch (error) {
// If server config fails to load, skip DDoS protection
console.warn('Failed to load security config for middleware:', error);
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};