Skip to content

Commit

Permalink
Merge pull request #491 from jsdelivr/refactor-limits
Browse files Browse the repository at this point in the history
Refactor limits
  • Loading branch information
MartinKolarik authored Feb 15, 2024
2 parents f1e5667 + 70d813d commit e430bfb
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 103 deletions.
6 changes: 4 additions & 2 deletions config/default.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ module.exports = {
// measurement result TTL in redis
resultTTL: 7 * 24 * 60 * 60, // 7 days
limits: {
global: 500,
location: 200,
anonymousTestsPerLocation: 200,
anonymousTestsPerMeasurement: 500,
authenticatedTestsPerLocation: 500,
authenticatedTestsPerMeasurement: 500,
},
globalDistribution: {
AF: 5,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/http/middleware/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Schema } from 'joi';
import type { ExtendedMiddleware } from '../../../types.js';

export const validate = (schema: Schema): ExtendedMiddleware => async (ctx, next) => {
const valid = schema.validate(ctx.request.body, { convert: true });
const valid = schema.validate(ctx.request.body, { convert: true, context: ctx.state });

if (valid.error) {
const fields = valid.error.details.map(field => [ field.path.join('.'), String(field?.message) ]);
Expand Down
14 changes: 4 additions & 10 deletions src/lib/malware/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,8 @@ export const joiValidate = (value: string, helpers?: CustomHelpers): string | Er
return String(value);
};

export const joiErrorMessage = (field = 'address'): string => `Provided ${field} is blacklisted.`;

export const joiSchemaErrorMessage = (field?: string): Record<string, string> => {
const message = joiErrorMessage(field);

return {
'ip.blacklisted': message,
'domain.blacklisted': message,
'any.blacklisted': message,
};
export const joiSchemaErrorMessages = {
'ip.blacklisted': `Provided address is blacklisted.`,
'domain.blacklisted': `Provided address is blacklisted.`,
'any.blacklisted': `Provided address is blacklisted.`,
};
4 changes: 2 additions & 2 deletions src/measurement/schema/command-schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Joi from 'joi';
import {
joiSchemaErrorMessage as joiMalwareSchemaErrorMessage,
joiSchemaErrorMessages as joiMalwareSchemaErrorMessages,
} from '../../lib/malware/client.js';
import {
joiValidateDomain,
Expand All @@ -11,7 +11,7 @@ import {
} from './utils.js';

export const schemaErrorMessages = {
...joiMalwareSchemaErrorMessage(),
...joiMalwareSchemaErrorMessages,
'ip.private': 'Private hostnames are not allowed.',
'domain.invalid': 'Provided target is not a valid domain name',
};
Expand Down
9 changes: 7 additions & 2 deletions src/measurement/schema/global-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import {
import { schema as locationSchema } from './location-schema.js';
import { GLOBAL_DEFAULTS } from './utils.js';

const measurementConfig = config.get<{limits: {global: number; location: number}}>('measurement');
const authenticatedTestsPerMeasurement = config.get<number>('measurement.limits.authenticatedTestsPerMeasurement');
const anonymousTestsPerMeasurement = config.get<number>('measurement.limits.anonymousTestsPerMeasurement');

export const schema = Joi.object({
type: Joi.string().valid('ping', 'traceroute', 'dns', 'mtr', 'http').insensitive().required(),
target: targetSchema,
measurementOptions: measurementSchema,
locations: locationSchema,
limit: Joi.number().min(1).max(measurementConfig.limits.global).default(GLOBAL_DEFAULTS.limit),
limit: Joi.number().min(1).when('$userId', {
is: Joi.exist(),
then: Joi.number().max(authenticatedTestsPerMeasurement),
otherwise: Joi.number().max(anonymousTestsPerMeasurement),
}).default(GLOBAL_DEFAULTS.limit),
inProgressUpdates: Joi.bool().default(GLOBAL_DEFAULTS.inProgressUpdates),
});
34 changes: 30 additions & 4 deletions src/measurement/schema/location-schema.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@

import anyAscii from 'any-ascii';
import Joi from 'joi';
import Joi, { CustomHelpers, ErrorReport } from 'joi';
import config from 'config';

import { continents, countries } from 'countries-list';
import { states } from '../../lib/location/states.js';
import { regionNames } from '../../lib/location/regions.js';
import { GLOBAL_DEFAULTS } from './utils.js';
import type { LocationWithLimit } from '../types.js';

const measurementConfig = config.get<{limits: {global: number; location: number}}>('measurement');
const authenticatedTestsPerMeasurement = config.get<number>('measurement.limits.authenticatedTestsPerMeasurement');
const anonymousTestsPerMeasurement = config.get<number>('measurement.limits.anonymousTestsPerMeasurement');
const authenticatedTestsPerLocation = config.get<number>('measurement.limits.authenticatedTestsPerLocation');
const anonymousTestsPerLocation = config.get<number>('measurement.limits.anonymousTestsPerLocation');

const normalizeValue = (value: string): string => anyAscii(value);

export const sumOfLocationsLimits = (code: string, max: number) => (value: LocationWithLimit[], helpers: CustomHelpers): LocationWithLimit[] | ErrorReport => {
const sum = value.reduce((sum, location) => sum + (location.limit || 1), 0);

if (sum > max) {
return helpers.error(code);
}

return value;
};

export const schema = Joi.alternatives().try(
Joi.string(),
Joi.array().items(Joi.object().keys({
Expand All @@ -27,10 +41,22 @@ export const schema = Joi.alternatives().try(
asn: Joi.number().integer().positive(),
magic: Joi.string().min(1).custom(normalizeValue),
tags: Joi.array().items(Joi.string().min(1).max(128).lowercase().custom(normalizeValue)),
limit: Joi.number().min(1).max(measurementConfig.limits.location).when(Joi.ref('/limit'), {
limit: Joi.number().min(1).when('$userId', {
is: Joi.exist(),
then: Joi.number().max(authenticatedTestsPerLocation),
otherwise: Joi.number().max(anonymousTestsPerLocation),
}).when(Joi.ref('/limit'), {
is: Joi.exist(),
then: Joi.forbidden().messages({ 'any.unknown': 'limit per location is not allowed when a global limit is set' }),
otherwise: Joi.number().default(1),
}),
}).or('continent', 'region', 'country', 'state', 'city', 'network', 'asn', 'magic', 'tags')),
}).or('continent', 'region', 'country', 'state', 'city', 'network', 'asn', 'magic', 'tags'))
.when('$userId', {
is: Joi.exist(),
then: Joi.custom(sumOfLocationsLimits('limits.sum.auth', authenticatedTestsPerMeasurement)),
otherwise: Joi.custom(sumOfLocationsLimits('limits.sum.anon', anonymousTestsPerMeasurement)),
}).messages({
'limits.sum.auth': `Sum of limits must be less than or equal to ${authenticatedTestsPerMeasurement}`,
'limits.sum.anon': `Sum of limits must be less than or equal to ${anonymousTestsPerMeasurement}`,
}),
).default(GLOBAL_DEFAULTS.locations);
Loading

0 comments on commit e430bfb

Please sign in to comment.