Skip to content

Commit e183618

Browse files
committed
Add body-limit middleware and improve README
1 parent a1dc86c commit e183618

File tree

10 files changed

+1323
-24
lines changed

10 files changed

+1323
-24
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ console.log(routes); // [{ id: '...', method: 'GET', path: '/users/:id' }]
106106
app.clear();
107107
```
108108

109-
### 🧩 Middleware
109+
### 🧩 Middleware ([`@rabbit-company/web-middleware`](https://www.npmjs.com/package/@rabbit-company/web-middleware))
110110

111111
```js
112112
// Global middleware (chainable)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web-monorepo",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"description": "High-performance web framework monorepo",
55
"private": true,
66
"type": "module",

packages/core/README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@ const app = new Web<{ user?: { id: string } }>();
3838

3939
// Middleware
4040
app.use(async (ctx, next) => {
41-
console.log(`${ctx.req.method} ${ctx.req.url}`);
42-
await next();
41+
console.log(`${ctx.req.method} ${ctx.req.url}`);
42+
await next();
4343
});
4444

4545
// Routes
4646
app.get('/', (ctx) => ctx.text('Hello!'));
4747

4848
app.get('/users/:id', async (ctx) => {
49-
const user = await getUser(ctx.params.id);
50-
return ctx.json(user);
49+
const user = await getUser(ctx.params.id);
50+
return ctx.json(user);
5151
});
5252

5353
// Start server
@@ -106,7 +106,7 @@ console.log(routes); // [{ id: '...', method: 'GET', path: '/users/:id' }]
106106
app.clear();
107107
```
108108

109-
### 🧩 Middleware
109+
### 🧩 Middleware ([`@rabbit-company/web-middleware`](https://www.npmjs.com/package/@rabbit-company/web-middleware))
110110

111111
```js
112112
// Global middleware (chainable)
@@ -118,17 +118,17 @@ app.use(async (ctx, next) => {
118118

119119
// Path-specific middleware (chainable)
120120
app.use("/admin", adminAuthMiddleware)
121-
.use("POST", "/users", validateUserMiddleware);
121+
.use("POST", "/users", validateUserMiddleware);
122122

123123
// Middleware with removal capability
124124
const authId = app.addMiddleware('/admin', (ctx, next) => {
125-
// Authentication logic
126-
await next();
125+
// Authentication logic
126+
await next();
127127
});
128128

129129
const loggingId = app.addMiddleware(async (ctx, next) => {
130-
console.log(`${ctx.req.method} ${ctx.req.url}`);
131-
await next();
130+
console.log(`${ctx.req.method} ${ctx.req.url}`);
131+
await next();
132132
});
133133

134134
// Remove middleware

packages/core/jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"license": "MIT",
55
"exports": "./src/index.ts",
66
"publish": {

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"description": "High-performance web framework",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

packages/middleware/README.md

Lines changed: 225 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,20 @@ console.log("Server running at http://localhost:3000");
8181

8282
### 🔐 Authentication
8383

84-
- **Bearer Auth** - JWT/API token authentication
85-
- **Basic Auth** - HTTP Basic authentication
84+
- [**Bearer Auth**](#bearer-auth) - JWT/API token authentication
85+
- [**Basic Auth**](#basic-auth) - HTTP Basic authentication
8686

8787
### 🛡️ Security
8888

89-
- **CORS** - Cross-Origin Resource Sharing
90-
- **Rate Limiting** - Request rate limiting with multiple algorithms
89+
- [**CORS**](#cors) - Cross-Origin Resource Sharing
90+
- [**Rate Limit**](#rate-limit) - Request rate limiting with multiple algorithms
9191

9292
### 📊 Utils
9393

94-
- **Logger** - HTTP request/response logging with customizable formats
95-
- **Cache** - Response caching using pluggable backends like in-memory, LRU, or Redis for improved performance and reduced server load.
96-
- **IP Extract** - Parses the incoming request's IP address, respecting common proxy headers (X-Forwarded-For, X-Real-IP) and attaches it to the request context.
94+
- [**Logger**](#logger) - HTTP request/response logging with customizable formats
95+
- [**Body Limit**](#body-limit) - Limit the file size of the request body
96+
- [**Cache**](#cache) - Response caching using pluggable backends like in-memory, LRU, or Redis for improved performance and reduced server load
97+
- [**IP Extract**](#ip-extract) - Parses the incoming request's IP address, respecting common proxy headers (X-Forwarded-For, X-Real-IP) and attaches it to the request context
9798

9899
## 📚 Middleware Documentation
99100

@@ -349,6 +350,113 @@ app.use(cache({ storage: new CustomStorage() }));
349350
- Distributed caching with Redis backend
350351
- Memory efficient with configurable storage limits
351352
353+
### Body Limit
354+
355+
Protect your server from large request payloads with configurable size limits. Prevents memory exhaustion and ensures fair resource usage.
356+
357+
```js
358+
import { bodyLimit } from "@rabbit-company/web-middleware/body-limit";
359+
360+
// Basic usage - 1MB limit (default)
361+
app.use(bodyLimit());
362+
363+
// Custom size limit
364+
app.use(
365+
bodyLimit({
366+
maxSize: "5mb", // or 5242880 for bytes
367+
})
368+
);
369+
370+
// Different limits for different routes
371+
app.post("/api/upload/avatar", bodyLimit({ maxSize: "2mb" }), uploadAvatar);
372+
373+
app.post("/api/upload/video", bodyLimit({ maxSize: "100mb" }), uploadVideo);
374+
375+
// Limit only specific content types
376+
app.use(
377+
bodyLimit({
378+
maxSize: "10mb",
379+
contentTypes: ["application/json", "application/xml"],
380+
message: "JSON/XML payload too large",
381+
})
382+
);
383+
384+
// Skip limit for premium users
385+
app.use(
386+
bodyLimit({
387+
maxSize: "5mb",
388+
skip: async (ctx) => {
389+
const user = ctx.get("user");
390+
return user?.plan === "premium";
391+
},
392+
})
393+
);
394+
395+
// Custom error handling
396+
app.use(
397+
bodyLimit({
398+
maxSize: "1mb",
399+
message: (size, limit) => `Payload too large: ${(size / 1024).toFixed(2)}KB exceeds ${(limit / 1024).toFixed(2)}KB limit`,
400+
statusCode: 400, // Use 400 instead of default 413
401+
})
402+
);
403+
404+
// Include headers in size calculation
405+
app.use(
406+
bodyLimit({
407+
maxSize: "10kb",
408+
includeHeaders: true, // Total request size including headers
409+
})
410+
);
411+
412+
// File upload endpoint with strict limit
413+
app.post(
414+
"/api/documents",
415+
bodyLimit({
416+
maxSize: "10mb",
417+
contentTypes: ["multipart/form-data"],
418+
message: "Document size must not exceed 10MB",
419+
}),
420+
async (ctx) => {
421+
const formData = await ctx.req.formData();
422+
const file = formData.get("document");
423+
// Process file...
424+
return ctx.json({ success: true });
425+
}
426+
);
427+
```
428+
429+
#### Options:
430+
431+
- `maxSize`: Maximum allowed body size (number in bytes or string with units: "1kb", "5mb", "1gb")
432+
- `includeHeaders`: Include header size in limit calculation (default: false)
433+
- `message`: Error message - string or function(size, limit)
434+
- `statusCode`: HTTP status code when limit exceeded (default: 413)
435+
- `contentTypes`: Array of content types to apply limit to (default: all)
436+
- `skip`: Function to conditionally skip limit check
437+
438+
#### Size Format Examples:
439+
440+
- `100` or `"100"` - 100 bytes
441+
- `"100b"` - 100 bytes
442+
- `"10kb"` - 10 kilobytes (10,240 bytes)
443+
- `"5.5mb"` - 5.5 megabytes
444+
- `"1gb"` - 1 gigabyte
445+
446+
#### Security Benefits:
447+
448+
- Prevents memory exhaustion attacks
449+
- Protects against slowloris-style attacks
450+
- Ensures fair resource allocation
451+
- Reduces attack surface for buffer overflow exploits
452+
453+
#### Performance Notes:
454+
455+
- Uses Content-Length header for efficient early rejection
456+
- No body parsing required - fails fast for oversized requests
457+
- Minimal memory overhead
458+
- Works with streaming and non-streaming requests
459+
352460
### Bearer Auth
353461
354462
Token-based authentication for APIs.
@@ -567,7 +675,7 @@ app.use(
567675
- `preflightContinue`: Pass OPTIONS requests to next handler
568676
- `optionsSuccessStatus`: Status code for successful OPTIONS
569677
570-
### Rate Limiting
678+
### Rate Limit
571679
572680
Advanced rate limiting with multiple algorithms to prevent API abuse and ensure fair usage..
573681
@@ -724,6 +832,115 @@ console.log(`Active rate limits: ${limiter.getSize()}`);
724832
- `RateLimit-Algorithm`: Algorithm used
725833
- `Retry-After`: Seconds until retry (when limited)
726834
835+
### IP Extract
836+
837+
Securely extract client IP addresses from requests, handling various proxy configurations and preventing IP spoofing attacks.
838+
839+
```js
840+
import { ipExtract, getClientIp } from "@rabbit-company/web-middleware/ip-extract";
841+
842+
// Direct connection (no proxy)
843+
app.use(ipExtract("direct"));
844+
845+
// Behind Cloudflare
846+
app.use(ipExtract("cloudflare"));
847+
848+
// Behind AWS Load Balancer
849+
app.use(ipExtract("aws"));
850+
851+
// Behind nginx reverse proxy
852+
app.use(ipExtract("nginx"));
853+
854+
// Custom configuration
855+
app.use(
856+
ipExtract({
857+
trustProxy: true,
858+
trustedProxies: ["10.0.0.0/8", "172.16.0.0/12"],
859+
trustedHeaders: ["x-real-ip", "x-forwarded-for"],
860+
maxProxyChain: 3,
861+
logWarnings: true,
862+
})
863+
);
864+
865+
// Access the extracted IP
866+
app.get("/api/info", (ctx) => {
867+
const ip = getClientIp(ctx);
868+
// or directly: ctx.clientIp
869+
return ctx.json({
870+
clientIp: ip,
871+
country: geoip.lookup(ip)?.country,
872+
});
873+
});
874+
875+
// Rate limiting by IP
876+
app.use(ipExtract("cloudflare"));
877+
app.use(
878+
rateLimit({
879+
keyGenerator: (ctx) => getClientIp(ctx) || "unknown",
880+
})
881+
);
882+
883+
// Logging with real IPs
884+
app.use(ipExtract("nginx"));
885+
app.use(
886+
logger({
887+
getUserId: (ctx) => getClientIp(ctx),
888+
})
889+
);
890+
891+
// IP-based access control
892+
const ipWhitelist = ["192.168.1.0/24", "10.0.0.0/8"];
893+
894+
app.use("/admin", ipExtract("direct"), (ctx, next) => {
895+
const ip = getClientIp(ctx);
896+
if (!ip || !isIpInWhitelist(ip, ipWhitelist)) {
897+
return ctx.text("Access denied", 403);
898+
}
899+
return next();
900+
});
901+
902+
// Development mode (trusts all headers - NOT for production!)
903+
if (process.env.NODE_ENV === "development") {
904+
app.use(ipExtract("development"));
905+
}
906+
```
907+
908+
#### Presets:
909+
910+
- `"direct"` - No proxy, direct connections only
911+
- `"cloudflare"` - Behind Cloudflare (auto-configures CF IPs)
912+
- `"aws"` - Behind AWS ALB/ELB
913+
- `"gcp"` - Behind Google Cloud Load Balancer
914+
- `"azure"` - Behind Azure Application Gateway
915+
- `"vercel"` - Behind Vercel's edge network
916+
- `"nginx"` - Behind nginx reverse proxy
917+
- `"development"` - Trusts all headers (NEVER use in production!)
918+
919+
#### Options:
920+
921+
- `trustProxy`: Enable proxy header parsing (default: false)
922+
- `trustedProxies`: List of trusted proxy IPs/CIDR ranges
923+
- `trustedHeaders`: Headers to check in order (default: ["x-forwarded-for", "x-real-ip"])
924+
- `maxProxyChain`: Maximum proxy chain length to prevent attacks (default: 5)
925+
- `cloudProvider`: Auto-configure for cloud provider ("aws", "cloudflare", "gcp", "azure", "vercel")
926+
- `logWarnings`: Log suspicious activity (default: false)
927+
928+
#### Security Features:
929+
930+
- **IP Spoofing Prevention**: Only trusts headers from configured proxies
931+
- **Chain Length Limits**: Prevents long X-Forwarded-For chains
932+
- **CIDR Support**: Configure trusted proxy ranges (e.g., "10.0.0.0/8")
933+
- **Cloud Provider Detection**: Pre-configured secure settings for major providers
934+
- **IPv4/IPv6 Support**: Full support for both protocols
935+
936+
#### Important Notes:
937+
938+
- Always use HTTPS in production to prevent header injection
939+
- Configure `trustedProxies` to match your infrastructure
940+
- The `development` preset is insecure - only for local testing
941+
- Test your configuration with tools like `curl -H "X-Forwarded-For: fake"`
942+
- Consider using cloud provider presets for automatic secure configuration
943+
727944
## 📦 Dependencies
728945
729946
- `@rabbit-company/web` - Core web framework (peer dependency)

packages/middleware/jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web-middleware",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"license": "MIT",
55
"exports": {
66
"./basic-auth": "./src/basic-auth.ts",

packages/middleware/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web-middleware",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"description": "Official middleware collection for Rabbit Company Web Framework",
55
"type": "module",
66
"homepage": "https://github.com/Rabbit-Company/Web-JS",
@@ -17,6 +17,10 @@
1717
"types": "./dist/bearer-auth.d.ts",
1818
"import": "./dist/bearer-auth.js"
1919
},
20+
"./body-limit": {
21+
"types": "./dist/body-limit.d.ts",
22+
"import": "./dist/body-limit.js"
23+
},
2024
"./cache": {
2125
"types": "./dist/cache.d.ts",
2226
"import": "./dist/cache.js"
@@ -66,6 +70,7 @@
6670
"middleware",
6771
"basic-auth",
6872
"bearer-auth",
73+
"body-limit",
6974
"cache",
7075
"cors",
7176
"ip-extract",

0 commit comments

Comments
 (0)