Skip to content

Commit 81d645b

Browse files
committed
wrote the framework for the express middleware.
1 parent e3cddeb commit 81d645b

File tree

6 files changed

+225
-26
lines changed

6 files changed

+225
-26
lines changed

package-lock.json

Lines changed: 127 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
},
5252
"dependencies": {
5353
"graphql": "^16.5.0",
54+
"ioredis": "^5.0.5",
5455
"redis": "^4.1.0"
5556
}
5657
}

src/@types/rateLimit.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface RateLimiter {
1515

1616
interface RateLimiterResponse {
1717
success: boolean;
18-
tokens?: number;
18+
tokens: number;
1919
}
2020

2121
interface RedisBucket {

src/middleware/index.ts

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { RedisClientOptions } from 'redis';
2-
import { Request, Response, NextFunction, RequestHandler } from 'express';
1+
import Redis, { RedisOptions } from 'ioredis';
32
import { GraphQLSchema } from 'graphql/type/schema';
4-
import { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights';
3+
import { Request, Response, NextFunction, RequestHandler } from 'express';
4+
5+
import buildTypeWeightsFromSchema, { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights';
6+
import setupRateLimiter from './rateLimiterSetup';
7+
import getQueryTypeComplexity from '../analysis/typeComplexityAnalysis';
58

69
// FIXME: Will the developer be responsible for first parsing the schema from a file?
710
// Can consider accepting a string representing a the filepath to a schema
@@ -12,32 +15,76 @@ import { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights';
1215
* @param {RateLimiterSelection} rateLimiter Specify rate limiting algorithm to be used
1316
* @param {RateLimiterOptions} options Specify the appropriate options for the selected rateLimiter
1417
* @param {GraphQLSchema} schema GraphQLSchema object
15-
* @param {RedisClientOptions} redisClientOptions valid node-redis connection options. See https://github.com/redis/node-redis/blob/HEAD/docs/client-configuration.md
18+
* @param {RedisClientOptions} RedisOptions // TODO add dsecription
1619
* @param {TypeWeightConfig} typeWeightConfig Optional type weight configuration for the GraphQL Schema.
1720
* Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
1821
* @returns {RequestHandler} express middleware that computes the complexity of req.query and calls the next middleware
1922
* if the query is allowed or sends a 429 status if the request is blocked
2023
* @throws ValidationError if GraphQL Schema is invalid
2124
*/
2225
export function expressRateLimiter(
23-
rateLimiter: RateLimiterSelection,
26+
rateLimiterAlgo: RateLimiterSelection,
2427
rateLimiterOptions: RateLimiterOptions,
2528
schema: GraphQLSchema,
26-
redisClientOptions: RedisClientOptions,
29+
redisClientOptions: RedisOptions,
2730
typeWeightConfig: TypeWeightConfig = defaultTypeWeightsConfig
2831
): RequestHandler {
29-
const timeStamp = new Date().valueOf();
30-
// TODO: Parse the schema to create a TypeWeightObject. Throw ValidationError if schema is invalid
31-
// TODO: Connect to Redis store using provided options. Default to localhost:6379
32-
// TODO: Configure the selected RateLimtier
33-
// TODO: Configure the complexity analysis algorithm to run for incoming requests
32+
/**
33+
* build the type weight object, create the redis client and instantiate the ratelimiter
34+
* before returning the express middleware that calculates query complexity and throttles the requests
35+
*/
36+
// TODO: Throw ValidationError if schema is invalid
37+
const typeWeightObject = buildTypeWeightsFromSchema(schema, typeWeightConfig);
38+
// TODO: Throw error if connection is unsuccessful
39+
const redisClient = new Redis(redisClientOptions); // Default port is 6379 automatically
40+
const rateLimiter = setupRateLimiter(rateLimiterAlgo, rateLimiterOptions, redisClient);
41+
42+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
43+
const requestTimestamp = new Date().valueOf();
44+
const { query }: { query: string } = req.body;
45+
if (!query) {
46+
// FIXME: Throw an error here? Code currently passes this on to whatever is next
47+
console.log('There is no query on the request');
48+
return next();
49+
}
50+
51+
/**
52+
* There are numorous ways to get the ip address off of the request object.
53+
* - the header 'x-forward-for' will hold the originating ip address if a proxy is placed infront of the server. This would be commen for a production build.
54+
* - req.ips wwill hold an array of ip addresses in'x-forward-for' header. client is likely at index zero
55+
* - req.ip will have the ip address
56+
* - req.socket.remoteAddress is an insatnce of net.socket which is used as another method of getting the ip address
57+
*
58+
* req.ip and req.ips will worx in express but not with other frameworks
59+
*/
60+
const ip: string = req.ips[0] || req.ip;
61+
// FIXME: this will only work with type complexity
62+
const queryComplexity = getQueryTypeComplexity(query, typeWeightObject);
3463

35-
const middleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
36-
// TODO: Parse query from req.query, compute complexity and pass necessary info to rate limiter
37-
// TODO: Call next if query is successful, send 429 status if query blocked, call next(err) with any thrown errors
38-
next(Error('Express rate limiting middleware not implemented'));
64+
try {
65+
const rateLimiterResponse = await rateLimiter.processRequest(
66+
ip,
67+
requestTimestamp,
68+
queryComplexity
69+
);
70+
if (rateLimiterResponse.success === false) {
71+
// TODO: add a header 'Retry-After' with the time to wait untill next query will succeed
72+
res.status(429).json({
73+
timestamp: requestTimestamp,
74+
complexity: queryComplexity,
75+
tokens: rateLimiterResponse.tokens,
76+
});
77+
}
78+
res.locals.graphqlGate = {
79+
timestamp: requestTimestamp,
80+
complexity: queryComplexity,
81+
tokens: rateLimiterResponse.tokens,
82+
};
83+
return next();
84+
} catch (err) {
85+
return next(err);
86+
}
3987
};
40-
return middleware;
4188
}
4289

4390
export default expressRateLimiter;

0 commit comments

Comments
 (0)