Skip to content

Commit

Permalink
feat: wrapped metrics POST requests in rate limiter
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswk committed Oct 17, 2023
1 parent 7f13619 commit 3ae3792
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 8 deletions.
6 changes: 6 additions & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ exports[`should create default config 1`] = `
"host": undefined,
"port": 4242,
},
"metricsRateLimiting": {
"clientMetricsMax": 100,
"clientRegisterMax": 100,
"frontendMetricsMax": 100,
"frontendRegisterMax": 100,
},
"preHook": undefined,
"preRouterHook": undefined,
"prometheusApi": undefined,
Expand Down
36 changes: 36 additions & 0 deletions src/lib/create-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ICspDomainConfig,
ICspDomainOptions,
IClientCachingOption,
IMetricsRateLimiting,
} from './types/option';
import { getDefaultLogProvider, LogLevel, validateLogProvider } from './logger';
import { defaultCustomAuthDenyAll } from './default-custom-auth-deny-all';
Expand Down Expand Up @@ -99,6 +100,38 @@ function loadClientCachingOptions(
]);
}

function loadMetricsRateLimitingConfig(
options: IUnleashOptions,
): IMetricsRateLimiting {
const clientMetricsMax = parseEnvVarNumber(
process.env.REGISTER_CLIENT_RATE_LIMIT,
100,
);
const clientRegisterMax: number = parseEnvVarNumber(
process.env.CLIENT_METRICS_RATE_LIMIT,
100,
);
const frontendRegisterMax = parseEnvVarNumber(
process.env.REGISTER_FRONTEND_RATE_LIMIT,
100,
);
const frontendMetricsMax = parseEnvVarNumber(
process.env.FRONTEND_METRICS_RATE_LIMIT,
100,
);
const defaultRateLimitOptions: IMetricsRateLimiting = {
clientMetricsMax,
clientRegisterMax,
frontendRegisterMax,
frontendMetricsMax,
};

return mergeAll([
defaultRateLimitOptions,
options.metricsRateLimiting ?? {},
]);
}

function loadUI(options: IUnleashOptions): IUIConfig {
const uiO = options.ui || {};
const ui: IUIConfig = {
Expand Down Expand Up @@ -490,6 +523,8 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
Boolean(options.enterpriseVersion) &&
ui.environment?.toLowerCase() !== 'pro';

const metricsRateLimiting = loadMetricsRateLimitingConfig(options);

return {
db,
session,
Expand Down Expand Up @@ -523,6 +558,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
publicFolder: options.publicFolder,
disableScheduler: options.disableScheduler,
isEnterprise: isEnterprise,
metricsRateLimiting,
};
}

Expand Down
9 changes: 9 additions & 0 deletions src/lib/routes/client-api/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
emptyResponse,
getStandardResponses,
} from '../../openapi/util/standard-responses';
import rateLimit from 'express-rate-limit';
import { minutesToMilliseconds } from 'date-fns';

export default class ClientMetricsController extends Controller {
logger: Logger;
Expand Down Expand Up @@ -61,6 +63,13 @@ export default class ClientMetricsController extends Controller {
204: emptyResponse,
},
}),
rateLimit({
windowMs: minutesToMilliseconds(1),
max: config.metricsRateLimiting.clientMetricsMax,
validate: false,
standardHeaders: true,
legacyHeaders: false,
}),
],
});
}
Expand Down
9 changes: 9 additions & 0 deletions src/lib/routes/client-api/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { OpenApiService } from '../../services/openapi-service';
import { emptyResponse } from '../../openapi/util/standard-responses';
import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { ClientApplicationSchema } from '../../openapi/spec/client-application-schema';
import rateLimit from 'express-rate-limit';
import { minutesToMilliseconds } from 'date-fns';

export default class RegisterController extends Controller {
logger: Logger;
Expand Down Expand Up @@ -48,6 +50,13 @@ export default class RegisterController extends Controller {
requestBody: createRequestSchema('clientApplicationSchema'),
responses: { 202: emptyResponse },
}),
rateLimit({
windowMs: minutesToMilliseconds(1),
max: config.metricsRateLimiting.clientRegisterMax,
validate: false,
standardHeaders: true,
legacyHeaders: false,
}),
],
});
}
Expand Down
16 changes: 16 additions & 0 deletions src/lib/routes/proxy-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { enrichContextWithIp } from '../../proxy';
import { corsOriginMiddleware } from '../../middleware';
import NotImplementedError from '../../error/not-implemented-error';
import NotFoundError from '../../error/notfound-error';
import rateLimit from 'express-rate-limit';
import { minutesToMilliseconds } from 'date-fns';

interface ApiUserRequest<
PARAM = any,
Expand Down Expand Up @@ -112,6 +114,13 @@ export default class ProxyController extends Controller {
...getStandardResponses(400, 401, 404),
},
}),
rateLimit({
windowMs: minutesToMilliseconds(1),
max: config.metricsRateLimiting.frontendMetricsMax,
validate: false,
standardHeaders: true,
legacyHeaders: false,
}),
],
});

Expand All @@ -133,6 +142,13 @@ export default class ProxyController extends Controller {
...getStandardResponses(400, 401, 404),
},
}),
rateLimit({
windowMs: minutesToMilliseconds(1),
max: config.metricsRateLimiting.frontendRegisterMax,
validate: false,
standardHeaders: true,
legacyHeaders: false,
}),
],
});

Expand Down
12 changes: 4 additions & 8 deletions src/lib/routes/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,10 @@ export const handleErrors: (
error: Error,
) => void = (res, logger, error) => {
if (createError.isHttpError(error)) {
return (
res
// @ts-expect-error http errors all have statuses, but there are no
// types provided
.status(error.status ?? 400)
.json({ message: error.message })
.end()
);
return res
.status(error.status ?? 400)
.json({ message: error.message })
.end();
}

const finalError =
Expand Down
9 changes: 9 additions & 0 deletions src/lib/types/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export interface IUnleashOptions {
prometheusApi?: string;
publicFolder?: string;
disableScheduler?: boolean;
metricsRateLimiting?: Partial<IMetricsRateLimiting>;
}

export interface IEmailOption {
Expand Down Expand Up @@ -185,6 +186,13 @@ interface IFrontendApi {
refreshIntervalInMs: number;
}

export interface IMetricsRateLimiting {
clientMetricsMax: number;
clientRegisterMax: number;
frontendMetricsMax: number;
frontendRegisterMax: number;
}

export interface IUnleashConfig {
db: IDBOption;
session: ISessionOption;
Expand Down Expand Up @@ -212,6 +220,7 @@ export interface IUnleashConfig {
inlineSegmentConstraints: boolean;
segmentValuesLimit: number;
strategySegmentsLimit: number;
metricsRateLimiting: IMetricsRateLimiting;
clientFeatureCaching: IClientCachingOption;
accessControlMaxAge: number;
prometheusApi?: string;
Expand Down

0 comments on commit 3ae3792

Please sign in to comment.