-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
288 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { Context, Middleware, Next } from '../deps.ts'; | ||
import { responseError } from '../utils.ts'; | ||
|
||
class RequestStore { | ||
private count: number; | ||
private timestamp: number; | ||
|
||
constructor(count: number, timestamp: number) { | ||
this.count = count; | ||
this.timestamp = timestamp; | ||
} | ||
|
||
public getCount(): number { | ||
return this.count; | ||
} | ||
|
||
public getTimestamp(): number { | ||
return this.timestamp; | ||
} | ||
} | ||
|
||
interface RateLimitParams { | ||
limit: number; | ||
windowMs: number; | ||
} | ||
|
||
export default class RateLimit { | ||
private map: Map<string, RequestStore>; | ||
|
||
constructor() { | ||
this.map = new Map(); | ||
} | ||
|
||
public middleware(params: RateLimitParams): Middleware { | ||
const { limit, windowMs } = params; | ||
|
||
return async (context: Context, next: Next) => { | ||
const ip = context.request.ip; | ||
const timestamp = Date.now(); | ||
|
||
if (!this.map.has(ip)) { | ||
this.updateRate(context, limit, limit - 1, windowMs, ip, 1, timestamp); | ||
return await next(); | ||
} | ||
|
||
const requestStore = this.map.get(ip) as RequestStore; | ||
const diff = timestamp - requestStore.getTimestamp(); | ||
|
||
if (diff > windowMs) { | ||
this.updateRate(context, limit, limit - 1, windowMs, ip, 1, timestamp); | ||
return await next(); | ||
} | ||
|
||
const count = requestStore.getCount() + 1; | ||
if (count > limit) { | ||
return responseError(context, new Error('Too many requests'), 429); | ||
} | ||
|
||
this.updateRate(context, limit, limit - count, windowMs, ip, count, timestamp); | ||
return await next(); | ||
}; | ||
} | ||
|
||
private updateRate( | ||
context: Context, | ||
limit: number, | ||
remaining: number, | ||
reset: number, | ||
ip: string, | ||
count: number, | ||
timestamp: number, | ||
) { | ||
context.response.headers.set('X-RateLimit-Limit', limit.toString()); | ||
context.response.headers.set('X-RateLimit-Remaining', remaining.toString()); | ||
context.response.headers.set('X-RateLimit-Reset', reset.toString()); | ||
|
||
this.map.set(ip, new RequestStore(count, timestamp)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import Helmet from '../../src/middleware/helmet.ts'; | ||
import { Context, Next } from '../../src/deps.ts'; | ||
import { assertEquals } from '../deps.ts'; | ||
|
||
const testTitle = (description: string) => `Helmet middleware - ${description}`; | ||
|
||
const newRequest = () => { | ||
return { | ||
request: { | ||
ip: 'localhost', | ||
}, | ||
response: { | ||
status: 0, | ||
body: {}, | ||
headers: new Headers(), | ||
}, | ||
}; | ||
}; | ||
|
||
Deno.test({ | ||
name: testTitle('it should set the headers'), | ||
async fn() { | ||
const helmet = new Helmet(); | ||
const middleware = helmet.middleware(); | ||
|
||
const next = () => { | ||
return; | ||
}; | ||
|
||
const req = newRequest(); | ||
await middleware(req as Context, next as Next); | ||
|
||
assertEquals(req.response.headers.get('Server'), 'Deno'); | ||
assertEquals(req.response.headers.get('Access-Control-Allow-Origin'), '*'); | ||
assertEquals( | ||
req.response.headers.get('Content-Security-Policy'), | ||
"default-src 'self' 'unsafe-inline' 'unsafe-eval' data: https: http:; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none';", | ||
); | ||
assertEquals(req.response.headers.get('X-Content-Type-Options'), 'nosniff'); | ||
assertEquals(req.response.headers.get('X-Frame-Options'), 'DENY'); | ||
assertEquals(req.response.headers.get('X-XSS-Protection'), '1; mode=block'); | ||
assertEquals(req.response.headers.get('Strict-Transport-Security'), 'max-age=31536000; includeSubDomains; preload'); | ||
assertEquals(req.response.headers.get('Referrer-Policy'), 'no-referrer'); | ||
assertEquals( | ||
req.response.headers.get('Permissions-Policy'), | ||
'geolocation=(), microphone=(), camera=(), payment=()', | ||
); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import RateLimit from '../../src/middleware/rate-limit.ts'; | ||
import { Context, Next } from '../../src/deps.ts'; | ||
import { assertEquals } from '../deps.ts'; | ||
|
||
const testTitle = (description: string) => `RateLimit middleware - ${description}`; | ||
|
||
const newRequest = () => { | ||
return { | ||
request: { | ||
ip: 'localhost', | ||
}, | ||
response: { | ||
status: 0, | ||
body: {}, | ||
headers: new Headers(), | ||
}, | ||
}; | ||
}; | ||
|
||
Deno.test({ | ||
name: testTitle('it should return 429 error when limit is exceeded'), | ||
async fn() { | ||
const rateLimit = new RateLimit(); | ||
const middleware = rateLimit.middleware({ | ||
limit: 1, | ||
windowMs: 1000, | ||
}); | ||
|
||
const next = () => { | ||
return; | ||
}; | ||
|
||
const req1 = newRequest(); | ||
await middleware(req1 as Context, next as Next); | ||
assertEquals(req1.response.status, 0); | ||
assertEquals(req1.response.headers.get('X-RateLimit-Limit'), '1'); | ||
assertEquals(req1.response.headers.get('X-RateLimit-Remaining'), '0'); | ||
|
||
const req2 = newRequest(); | ||
await middleware(req2 as Context, next as Next); | ||
assertEquals(req2.response.status, 429); | ||
assertEquals(req2.response.body, { error: 'Too many requests' }); | ||
}, | ||
}); | ||
|
||
Deno.test({ | ||
name: testTitle('it should reset counter after windowMs'), | ||
async fn() { | ||
const rateLimit = new RateLimit(); | ||
const middleware = rateLimit.middleware({ | ||
limit: 1, | ||
windowMs: 500, | ||
}); | ||
|
||
const next = () => { | ||
return; | ||
}; | ||
|
||
const req1 = newRequest(); | ||
await middleware(req1 as Context, next as Next); | ||
assertEquals(req1.response.status, 0); | ||
|
||
const req2 = newRequest(); | ||
await middleware(req2 as Context, next as Next); | ||
assertEquals(req2.response.status, 429); | ||
assertEquals(req2.response.body, { error: 'Too many requests' }); | ||
|
||
await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
const req3 = newRequest(); | ||
await middleware(req3 as Context, next as Next); | ||
assertEquals(req3.response.status, 0); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters