1- import { RedisClientOptions } from 'redis' ;
2- import { Request , Response , NextFunction , RequestHandler } from 'express' ;
1+ import Redis , { RedisOptions } from 'ioredis' ;
32import { 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 */
2225export 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
4390export default expressRateLimiter ;
0 commit comments