Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update RedisCache and rate limit related libraries #1290

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
16e8c6e
Update rate limit with redis store
qtomlinson Feb 21, 2025
31f4f69
Update Redis Cache
qtomlinson Feb 22, 2025
55fa892
Use quit() instead of disconnect() and throw if connecting to redis f…
qtomlinson Feb 24, 2025
a46fbf9
Handle breaking change in express-rate-limit 7.0.0
qtomlinson Feb 24, 2025
69be41f
Use TestContainers to run redis based integration tests
qtomlinson Feb 24, 2025
fa422ef
Refactor
qtomlinson Feb 25, 2025
6bc9978
Use default sandbox
qtomlinson Feb 25, 2025
b8f52ec
Turn of logging in definitionServiceTest
qtomlinson Feb 26, 2025
ba04bf7
Fix unit tests
qtomlinson Feb 26, 2025
84129f3
Add default logger fallback
qtomlinson Feb 26, 2025
12f85d3
Share redis client across api and batch rate limiting and caching
qtomlinson Feb 27, 2025
a9ad0be
Add logging to integration test setup
qtomlinson Mar 6, 2025
e3d72a2
Adjust timeout for integration test setup
qtomlinson Mar 6, 2025
a4c61bf
Fix linting errors
qtomlinson Mar 6, 2025
d0160f4
Disable the unstable unit test.
qtomlinson Mar 6, 2025
99a9aa4
Minor Refactor
qtomlinson Mar 7, 2025
f0fc99c
Allow more time to set up integration tests
qtomlinson Mar 10, 2025
1d38d1c
Use MiddlewareDelegate to delay construction of rate limiter after re…
qtomlinson Mar 14, 2025
ea769d6
Address review comments
qtomlinson Mar 14, 2025
7edecf1
Add default values and error handling when constructing rate-limiting…
qtomlinson Mar 14, 2025
5777bb0
Add more tests
qtomlinson Mar 14, 2025
f101ef3
Move health check ahead of rate limiting and refactor
qtomlinson Mar 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 5 additions & 42 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ const morgan = require('morgan')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const cors = require('cors')
const rateLimit = require('express-rate-limit')
const rateLimitRedisStore = require('rate-limit-redis')
const redis = require('redis')

const helmet = require('helmet')
const serializeError = require('serialize-error')
Expand All @@ -17,6 +14,8 @@ const passport = require('passport')
const swaggerUi = require('swagger-ui-express')
const loggerFactory = require('./providers/logging/logger')
const routesVersioning = require('express-routes-versioning')()
const { setupApiRateLimiterAfterCachingInit, setupBatchApiRateLimiterAfterCachingInit } = require('./lib/rateLimit')

const v1 = '1.0.0'

function createApp(config) {
Expand Down Expand Up @@ -132,59 +131,23 @@ function createApp(config) {
app.use(config.auth.service.middleware())

app.set('trust-proxy', true)
app.use('/', require('./routes/index')(config.buildsha, config.appVersion))

// If Redis is configured for caching, connect to it
const client = config.caching.caching_redis_service
? redis.createClient(6380, config.caching.caching_redis_service, {
auth_pass: config.caching.caching_redis_api_key,
tls: { servername: config.caching_redis_service }
})
: undefined

// rate-limit the remaining routes
const apiLimiter = config.caching.caching_redis_service
? rateLimit({
store: new rateLimitRedisStore({
client: client,
prefix: 'api'
}),
windowMs: config.limits.windowSeconds * 1000,
max: config.limits.max
})
: rateLimit({
windowMs: config.limits.windowSeconds * 1000,
max: config.limits.max
})

app.use(apiLimiter)
app.use(setupApiRateLimiterAfterCachingInit(config, cachingService))

// Use a (potentially lower) different API limit
// for batch API request
// for now, these include
// * POST /definitions
// * POST /curations
// * POST /notices
const batchApiLimiter = config.caching.caching_redis_service
? rateLimit({
store: new rateLimitRedisStore({
client: client,
prefix: 'batch-api'
}),
windowMs: config.limits.batchWindowSeconds * 1000,
max: config.limits.batchMax
})
: rateLimit({
windowMs: config.limits.batchWindowSeconds * 1000,
max: config.limits.batchMax
})

const batchApiLimiter = setupBatchApiRateLimiterAfterCachingInit(config, cachingService)
app.post('/definitions', batchApiLimiter)
app.post('/curations', batchApiLimiter)
app.post('/notices', batchApiLimiter)

app.use(require('./middleware/querystring'))

app.use('/', require('./routes/index')(config.buildsha, config.appVersion))
app.use('/origins/github', require('./routes/originGitHub')())
app.use('/origins/crate', require('./routes/originCrate')())
const repoAccess = require('./lib/condaRepoAccess')()
Expand Down
Empty file modified bin/www
100755 → 100644
Empty file.
148 changes: 148 additions & 0 deletions lib/rateLimit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// (c) Copyright 2025, SAP SE and ClearlyDefined contributors. Licensed under the MIT license.
// SPDX-License-Identifier: MIT

const { RedisStore } = require('rate-limit-redis')
const { rateLimit } = require('express-rate-limit')
const { RedisCache } = require('../providers/caching/redis')
const logger = require('../providers/logging/logger')

class RateLimiter {
constructor(opts) {
this.options = opts
this.logger = opts.logger || logger()
}

// Initialize the rate limiter with optional store
initialize(store) {
if (!this._limiter) {
this.logger.debug('Creating rate limiter', this.options.limit)
const options = RateLimiter.buildOptions(this.options.limit, store)
this._limiter = rateLimit(options)
this.logger.debug('Rate limiter initialized')
}
return this
}

// Return the rate limiter middleware
get middleware() {
return this.initialize()._limiter
}

// Build rate limiter options
static buildOptions({ windowMs, max }, store) {
const opts = {
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false // Disable the `X-RateLimit-*` headers
}
if (max === 0) {
opts.skip = () => true //See breaking changes in express-rate-limit v7.0.0 https://github.com/express-rate-limit/express-rate-limit/releases/tag/v7.0.0
} else {
opts.limit = max //limit is preferred over max
opts.windowMs = windowMs
}
if (store) opts.store = store
return opts
}
}

class RedisBasedRateLimiter extends RateLimiter {
constructor(opts) {
super(opts)
this._client = opts.redis?.client
}

initialize() {
if (!this._client) throw new Error('Redis client is missing')
const store = RedisBasedRateLimiter.buildRedisStore(this._client, this.options.redis)
return super.initialize(store)
}

static buildRedisStore(client, { prefix }) {
return new RedisStore({
prefix,
sendCommand: (...args) => client.sendCommand(args)
})
}
}

function createRateLimiter(opts) {
return opts.redis ? new RedisBasedRateLimiter(opts) : new RateLimiter(opts)
}

function buildOpts({ windowSeconds = 0, max = 0 } = {}, cachingService, prefix, logger) {
const limit = { windowMs: windowSeconds * 1000, max }
const redis = cachingService instanceof RedisCache ? { client: cachingService.client, prefix } : undefined
return { limit, redis, logger }
}

function createApiLimiter({ config, cachingService, logger } = {}) {
return createRateLimiter(buildOpts(config?.limits, cachingService, 'api', logger))
}

function createBatchApiLimiter({ config, cachingService, logger } = {}) {
const { batchWindowSeconds, batchMax } = config?.limits || {}
const opts = buildOpts({ windowSeconds: batchWindowSeconds, max: batchMax }, cachingService, 'batch-api', logger)
return createRateLimiter(opts)
}

class AbstractMiddlewareDelegate {
constructor(opts) {
this.options = opts
this.logger = opts.logger || logger()
}

get middleware() {
return (request, response, next) => {
this._createMiddleware()
.catch(error => next(error))
.then(middleware => middleware(request, response, next))
}
}

async _createMiddleware() {
if (!this._innerMiddleware) {
try {
this._innerMiddleware = await this.createInnerMiddleware()
} catch (error) {
this.logger.error('Error creating inner middleware', error)
throw error
}
}
return this._innerMiddleware
}

async createInnerMiddleware() {
throw new Error('Not implemented')
}
}

class ApiMiddlewareDelegate extends AbstractMiddlewareDelegate {
async createInnerMiddleware() {
this.logger.debug('Creating api rate-limiting middleware')
await this.options.cachingService.initialize()
return createApiLimiter(this.options).middleware
}
}

class BatchApiMiddlewareDelegate extends AbstractMiddlewareDelegate {
async createInnerMiddleware() {
this.logger.debug('Creating batch api rate-limiting middleware')
await this.options.cachingService.initialize()
return createBatchApiLimiter(this.options).middleware
}
}

const setupApiRateLimiterAfterCachingInit = (config, cachingService, logger) =>
new ApiMiddlewareDelegate({ config, cachingService, logger }).middleware

const setupBatchApiRateLimiterAfterCachingInit = (config, cachingService, logger) =>
new BatchApiMiddlewareDelegate({ config, cachingService, logger }).middleware

module.exports = {
createApiLimiter,
createBatchApiLimiter,
RedisBasedRateLimiter,
RateLimiter,
setupApiRateLimiterAfterCachingInit,
setupBatchApiRateLimiterAfterCachingInit
}
Loading