-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(middleware): feat(middleware): add ratelimit middleware (#72)
add ratelimit middleware. GH-71
- Loading branch information
1 parent
0eb2382
commit 8dc4889
Showing
21 changed files
with
276 additions
and
12 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
File renamed without changes.
File renamed without changes.
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
3 changes: 1 addition & 2 deletions
3
...s__/acceptance/rate-limiter.acceptance.ts → ...ion-acceptance/rate-limiter.acceptance.ts
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
1 change: 1 addition & 0 deletions
1
src/__tests__/acceptance/ratelimit-middleware-acceptance/README.md
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 @@ | ||
# Acceptance tests |
39 changes: 39 additions & 0 deletions
39
src/__tests__/acceptance/ratelimit-middleware-acceptance/fixtures/application.ts
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,39 @@ | ||
import {BootMixin} from '@loopback/boot'; | ||
import {ApplicationConfig} from '@loopback/core'; | ||
import {RepositoryMixin} from '@loopback/repository'; | ||
import {RestApplication} from '@loopback/rest'; | ||
import {ServiceMixin} from '@loopback/service-proxy'; | ||
import path from 'path'; | ||
import {RateLimiterComponent, RateLimitSecurityBindings} from '../../../..'; | ||
import {TestController} from '../../test.controller'; | ||
|
||
import {MySequence} from './middleware.sequence'; | ||
import {StoreProvider} from '../../store.provider'; | ||
export {ApplicationConfig}; | ||
export class TestApplication extends BootMixin( | ||
ServiceMixin(RepositoryMixin(RestApplication)), | ||
) { | ||
constructor(options: ApplicationConfig = {}) { | ||
super(options); | ||
|
||
this.sequence(MySequence); | ||
|
||
this.static('/', path.join(__dirname, '../public')); | ||
this.bind(RateLimitSecurityBindings.RATELIMITCONFIG).to({ | ||
RatelimitActionMiddleware: true, | ||
}); | ||
this.component(RateLimiterComponent); | ||
|
||
this.projectRoot = __dirname; | ||
this.controller(TestController); | ||
this.bind(RateLimitSecurityBindings.DATASOURCEPROVIDER).toProvider( | ||
StoreProvider, | ||
); | ||
|
||
this.bind(RateLimitSecurityBindings.CONFIG).to({ | ||
name: 'inMemory', | ||
max: 5, | ||
windowMs: 2000, | ||
}); | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
src/__tests__/acceptance/ratelimit-middleware-acceptance/fixtures/middleware.sequence.ts
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,3 @@ | ||
import {MiddlewareSequence} from '@loopback/rest'; | ||
|
||
export class MySequence extends MiddlewareSequence {} |
23 changes: 23 additions & 0 deletions
23
src/__tests__/acceptance/ratelimit-middleware-acceptance/helper.ts
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,23 @@ | ||
import { | ||
Client, | ||
createRestAppClient, | ||
givenHttpServerConfig, | ||
} from '@loopback/testlab'; | ||
import {TestApplication} from './fixtures/application'; | ||
export async function setUpApplication(): Promise<AppWithClient> { | ||
const app = new TestApplication({ | ||
rest: givenHttpServerConfig(), | ||
}); | ||
|
||
await app.boot(); | ||
await app.start(); | ||
|
||
const client = createRestAppClient(app); | ||
|
||
return {app, client}; | ||
} | ||
|
||
export interface AppWithClient { | ||
app: TestApplication; | ||
client: Client; | ||
} |
65 changes: 65 additions & 0 deletions
65
src/__tests__/acceptance/ratelimit-middleware-acceptance/rate-limiter.acceptance.ts
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,65 @@ | ||
import {Client} from '@loopback/testlab'; | ||
import {memoryStore} from '../store.provider'; | ||
import {TestApplication} from './fixtures/application'; | ||
import {setUpApplication} from './helper'; | ||
describe('Acceptance Test Cases', () => { | ||
let app: TestApplication; | ||
let client: Client; | ||
|
||
before('setupApplication', async () => { | ||
({app, client} = await setUpApplication()); | ||
}); | ||
afterEach(async () => { | ||
await clearStore(); | ||
}); | ||
|
||
after(async () => app.stop()); | ||
|
||
it('should hit end point when number of requests is less than max requests allowed', async () => { | ||
//Max request is set to 5 while binding | ||
for (let i = 0; i < 4; i++) { | ||
await client.get('/test').expect(200); | ||
} | ||
}); | ||
|
||
it('should hit end point when number of requests is equal to max requests allowed', async () => { | ||
//Max request is set to 5 while binding | ||
for (let i = 0; i < 5; i++) { | ||
await client.get('/test').expect(200); | ||
} | ||
}); | ||
|
||
it('should give error when number of requests is greater than max requests allowed', async () => { | ||
//Max request is set to 5 while binding | ||
for (let i = 0; i < 5; i++) { | ||
await client.get('/test').expect(200); | ||
} | ||
await client.get('/test').expect(429); | ||
}); | ||
|
||
it('should overwrite the default behaviour when rate limit decorator is applied', async () => { | ||
//Max request is set to 1 in decorator | ||
await client.get('/testDecorator').expect(200); | ||
await client.get('/testDecorator').expect(429); | ||
}); | ||
|
||
it('should throw no error if requests more than max are sent after window resets', async () => { | ||
//Max request is set to 5 while binding | ||
for (let i = 0; i < 5; i++) { | ||
await client.get('/test').expect(200); | ||
} | ||
setTimeout(() => { | ||
client | ||
.get('/test') | ||
.expect(200) | ||
.then( | ||
() => {}, | ||
() => {}, | ||
); | ||
}, 2000); | ||
}); | ||
|
||
async function clearStore() { | ||
memoryStore.resetAll(); | ||
} | ||
}); |
3 changes: 2 additions & 1 deletion
3
...s__/acceptance/fixtures/store.provider.ts → src/__tests__/acceptance/store.provider.ts
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
2 changes: 1 addition & 1 deletion
2
...__/acceptance/fixtures/test.controller.ts → src/__tests__/acceptance/test.controller.ts
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,2 @@ | ||
export * from './ratelimit.middleware'; | ||
export * from './middleware.enum'; |
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,3 @@ | ||
export enum RatelimitActionMiddlewareGroup { | ||
RATELIMIT = 'ratelimitAction', | ||
} |
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,88 @@ | ||
// @SONAR_STOP@ | ||
import {CoreBindings, inject, injectable, Next, Provider} from '@loopback/core'; | ||
import {Getter} from '@loopback/repository'; | ||
import { | ||
Request, | ||
Response, | ||
RestApplication, | ||
HttpErrors, | ||
Middleware, | ||
MiddlewareContext, | ||
asMiddleware, | ||
RestMiddlewareGroups, | ||
} from '@loopback/rest'; | ||
import * as RateLimit from 'express-rate-limit'; | ||
import {RateLimitSecurityBindings} from '../keys'; | ||
import {RateLimitMetadata, RateLimitOptions} from '../types'; | ||
import {RatelimitActionMiddlewareGroup} from './middleware.enum'; | ||
@injectable( | ||
asMiddleware({ | ||
group: RatelimitActionMiddlewareGroup.RATELIMIT, | ||
upstreamGroups: RestMiddlewareGroups.PARSE_PARAMS, | ||
downstreamGroups: [RestMiddlewareGroups.INVOKE_METHOD], | ||
}), | ||
) | ||
export class RatelimitMiddlewareProvider implements Provider<Middleware> { | ||
constructor( | ||
@inject.getter(RateLimitSecurityBindings.DATASOURCEPROVIDER) | ||
private readonly getDatastore: Getter<RateLimit.Store>, | ||
@inject.getter(RateLimitSecurityBindings.METADATA) | ||
private readonly getMetadata: Getter<RateLimitMetadata>, | ||
@inject(CoreBindings.APPLICATION_INSTANCE) | ||
private readonly application: RestApplication, | ||
@inject(RateLimitSecurityBindings.CONFIG, { | ||
optional: true, | ||
}) | ||
private readonly config?: RateLimitOptions, | ||
) {} | ||
|
||
value() { | ||
const middleware = async (ctx: MiddlewareContext, next: Next) => { | ||
await this.action(ctx.request, ctx.response); | ||
return next(); | ||
}; | ||
return middleware; | ||
} | ||
|
||
async action(request: Request, response: Response): Promise<void> { | ||
const enabledByDefault = this.config?.enabledByDefault ?? true; | ||
const metadata: RateLimitMetadata = await this.getMetadata(); | ||
const dataStore = await this.getDatastore(); | ||
if (metadata && !metadata.enabled) { | ||
return Promise.resolve(); | ||
} | ||
|
||
// Perform rate limiting now | ||
const promise = new Promise<void>((resolve, reject) => { | ||
// First check if rate limit options available at method level | ||
const operationMetadata = metadata ? metadata.options : {}; | ||
|
||
// Create options based on global config and method level config | ||
const opts = Object.assign({}, this.config, operationMetadata); | ||
|
||
if (dataStore) { | ||
opts.store = dataStore; | ||
} | ||
|
||
opts.message = new HttpErrors.TooManyRequests( | ||
opts.message?.toString() ?? 'Method rate limit reached !', | ||
); | ||
|
||
const limiter = RateLimit.default(opts); | ||
limiter(request, response, (err: unknown) => { | ||
if (err) { | ||
reject(err); | ||
} | ||
resolve(); | ||
}); | ||
}); | ||
if (enabledByDefault === true) { | ||
await promise; | ||
} else if (enabledByDefault === false && metadata && metadata.enabled) { | ||
await promise; | ||
} else { | ||
return Promise.resolve(); | ||
} | ||
} | ||
} | ||
// @SONAR_START@ |
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