0.2.4 • Published 1 year ago

tollbooth v0.2.4

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

Tollbooth

Run tests CodeQL npm version coverage

Tollbooth is a small utility (10kB raw JS) for Node.js, Deno, Express & AWS Lambda that throttles and limits number of requests per client using Redis.

  • TypeScript, Node, Deno
  • Express middleware
  • AWS Lambda HOF
  • Examples

Contents

Install

npm add tollbooth

or

yarn add tollbooth

How it works

  1. Checks how many requests does given token still have left.
  2. If the token was not given limit (i.e. setLimits was not called), rejects the request with Unauthorized.
  3. If the token does not have enough requests (i.e. limit == 0), rejects the request with LimitReached.
  4. Checks how many requests did the token make recently.
  5. If the token made more than X requests in the last N seconds (configurable), rejects the request with TooManyRequests.
  6. Otherwise, accepts the request with Ok.

Usage with Express

import express from 'express';
import Redis from 'ioredis';
import Tollbooth from 'tollbooth/express';

const redis = new Redis('redis://localhost:6379');

const app = express();

app.use(
  Tollbooth({
    redis,
    routes: [{ path: '/foo', method: 'get' }],
  }),
);

// setup the express app & start the server

By default, the token will be read from x-api-key header. See Configuration Options for customisation.

To manage tokens and limits, you can use Admin helpers.

import { setLimits, getLimit, removeLimits, UNLIMITED } from 'tollbooth';

// set tokens limits
// e.g. post request to create new account, cron job refreshing limits monthly
await setLimits(redis, [{ token: 'my_token', limit: 1_000 }]);
// token with no limit
await setLimits(redis, [{ token: 'my_token', limit: UNLIMITED }]);

// get token limit
// e.g. in user dashboard
const limit: number = await getLimit(redis, 'my_token');

// remove tokens
// e.g. on account termination
await removeLimits(redis, ['my_token']);

Usage with AWS Lambda

import { Context, APIGatewayProxyCallback, APIGatewayEvent } from 'aws-lambda';
import Redis from 'ioredis';
import Tollbooth from 'tollbooth/lambda';

const redis = new Redis('redis://localhost:6379');

const protect = Tollbooth({
  redis,
  routes: [{ path: '*', method: 'get' }],
});

function handle(_event: APIGatewayEvent, _context: Context, callback: APIGatewayProxyCallback) {
  callback(null, {
    statusCode: 200,
    body: JSON.stringify({ status: 'ok' }),
  });
}

export const handler = protect(handle);

By default, the token will be read from x-api-key header. See Configuration Options for options.

Manual usage

import Tollbooth, { TollboothCode, setLimits } from 'tollbooth';
import Redis from 'ioredis';

const redis = new Redis('redis://localhost:6379');
const protect = Tollbooth({
  redis,
  routes: [{ path: '/foo', method: 'get' }],
});

// ... application logic
await setLimits(redis, [{ token: 'my_token', limit: 5 }]);

const success = await protect({
  path: '/foo',
  method: 'get',
  token: 'my_token',
});

console.assert(success.code === TollboothCode.Ok);
console.log('Result', success);
// ... application logic

Return value

{
  // HTTP status code
  statusCode: number;
  // Internal code
  code: TollboothCode.TooManyRequests |
    TollboothCode.Unauthorized |
    TollboothCode.LimitReached |
    TollboothCode.Ok |
    TollboothCode.RedisError;
  // Human readable code
  message: 'TooManyRequests' | 'Unauthorized' | 'LimitReached' | 'Ok' | 'RedisError';
}

Configuration options

  • redis: Redis instance, e.g. ioredis
  • routes: List of protected routes
    • path: Relative path, e.g. /foo, or * to protect all paths with given method.
    • method: One of get, head, post, put, patch, delete, options
  • tokenHeaderName: (Only for Express and AWS Lambda) Name of the header containing token. Default x-api-key
  • errorHandler: (Only for Express and AWS Lambda) Custom error handler function with signature (res: express.Response | APIGatewayProxyCallback, error: tollbooth.TollboothError) => void
  • allowAnonymous: (Optional) If set to true, allows access without token. Default: false
  • debug: (Optional) If set to true, will enable console logging. Default: false
  • failOnExceptions: (Optional) If set to false, will not propagate exceptions (e.g. redis connection error), therefore allowing access. Default: true
  • throttleEnabled: (Optional) If set to false, turns off throttling. Default: true
  • throttleInterval: (Optional) Duration of the throttle interval in seconds. For example, when throttleInterval=2 and throttleLimit=10, it will allow max 10 requests per 2 seconds, or fail with 429 response. Default: 1
  • throttleLimit: (Optional) Maximum number of requests executed during the throttle interval. Default: 10.

Admin helpers

import Redis from 'ioredis';
import { getLimit, removeLimits, setLimits, UNLIMITED } from 'tollbooth';

const redis = new Redis('redis://localhost:6379');

// ... application logic

// set token1 with maximum of 1_000 requests
// set token2 with maximum of 1 request
// set token3 with unlimited requests
await setLimits(redis, [
  { token: 'token1', limit: 1_000 },
  { token: 'token2', limit: 1 },
  { token: 'token3', limit: UNLIMITED },
);

const currentLimit = await getLimit(redis, 'token1');
console.log({ currentLimit });
// { currentLimit: 1000 }

// removes token1
await removeLimits(redis, ['token1']);

const newLimit = await getLimit(redis, 'token1');
console.log({ newLimit });
// { newLimit: 0 }


// deletes all keys saved in redis
await evict(redis);

// ... application logic

Examples

See examples folder.

Running redis

Running locally

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

3rd party services

Benchmarks

Start redis on localhost:6379 and run

npm run benchmark

See benchmarks folder. Currently comparing with executing single redis call. Results on EC2 t4g.small instance with redis running locally.

incrByScalar x 13,199 ops/sec ±2.09% (83 runs sampled)
protect x 7,582 ops/sec ±1.48% (83 runs sampled)
incrByScalar x 62,546 calls took 5903 ms, made 62,547 redis calls
protect x 36,493 calls took 5963 ms, made 145,979 redis calls
total redis calls 208,526

Development

Build

npm run build

Run tests

Start redis on localhost:6379 and run

npm test
0.2.4

1 year ago

0.2.3

1 year ago

0.2.2

1 year ago

0.2.1

1 year ago

0.2.0

1 year ago

0.1.0

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago