diff --git a/ts/src/header-validator/validate-json.ts b/ts/src/header-validator/validate-json.ts index 631ef9a1ac..5e8233de1e 100644 --- a/ts/src/header-validator/validate-json.ts +++ b/ts/src/header-validator/validate-json.ts @@ -1,4 +1,3 @@ -import * as psl from 'psl' import * as uuid from 'uuid' import * as constants from '../constants' import { SourceType } from '../source-type' @@ -8,10 +7,14 @@ import { Maybe } from './maybe' import { CtxFunc, ItemErrorAction, + LengthOpts, clamp, isInteger, isInRange, + isLengthValid, matchesPattern, + suitableOrigin, + suitableSite, } from './validate' import * as validate from './validate' import * as privacy from '../flexible-event/privacy' @@ -156,34 +159,10 @@ function keyValues( }) } -type LengthOpts = { - minLength?: number - maxLength?: number - maxLengthErrSuffix?: string -} - function list(j: Json, ctx: Context): Maybe { return typeSwitch(j, ctx, { list: some }) } -function isLengthValid( - length: number, - ctx: Context, - { - minLength = 0, - maxLength = Infinity, - maxLengthErrSuffix = '', - }: LengthOpts = {} -): boolean { - if (length > maxLength || length < minLength) { - ctx.error( - `length must be in the range [${minLength}, ${maxLength}${maxLengthErrSuffix}]` - ) - return false - } - return true -} - function uint64(j: Json, ctx: Context): Maybe { return string(j, ctx) .filter( @@ -237,79 +216,14 @@ function hex128(j: Json, ctx: Context): Maybe { .map(BigInt) } -function suitableScope( - s: string, - ctx: Context, - label: string, - scope: (url: URL) => string, - rejectExtraComponents: boolean -): Maybe { - let url - try { - url = new URL(s) - } catch { - ctx.error('invalid URL') - return None - } - - if ( - url.protocol !== 'https:' && - !( - url.protocol === 'http:' && - (url.hostname === 'localhost' || url.hostname === '127.0.0.1') - ) - ) { - ctx.error('URL must use HTTP/HTTPS and be potentially trustworthy') - return None - } - - const scoped = scope(url) - if (url.toString() !== new URL(scoped).toString()) { - if (rejectExtraComponents) { - ctx.error( - `must not contain URL components other than ${label} (${scoped})` - ) - return None - } - ctx.warning( - `URL components other than ${label} (${scoped}) will be ignored` - ) - } - return some(scoped) -} - -function suitableOrigin( - j: Json, - ctx: Context, - rejectExtraComponents: boolean = false -): Maybe { - return string(j, ctx).map( - suitableScope, - ctx, - 'origin', - (u) => u.origin, - rejectExtraComponents - ) -} - -function suitableSite( - j: Json, - ctx: Context, - rejectExtraComponents: boolean = false -): Maybe { - return string(j, ctx).map( - suitableScope, - ctx, - 'site', - (u) => `${u.protocol}//${psl.get(u.hostname)}`, - rejectExtraComponents - ) -} - function destination(j: Json, ctx: Context): Maybe> { return typeSwitch(j, ctx, { string: (j) => suitableSite(j, ctx).map((s) => new Set([s])), - list: (j) => set(j, ctx, suitableSite, { minLength: 1, maxLength: 3 }), + list: (j) => + set(j, ctx, (j) => string(j, ctx).map(suitableSite, ctx), { + minLength: 1, + maxLength: 3, + }), }) } @@ -1432,14 +1346,16 @@ function aggregationCoordinatorOrigin( j: Json, ctx: RegistrationContext ): Maybe { - return suitableOrigin(j, ctx).filter((s) => { - if (!ctx.vsv.aggregationCoordinatorOrigins.includes(s)) { - const allowed = ctx.vsv.aggregationCoordinatorOrigins.join(', ') - ctx.error(`must be one of the following: ${allowed}`) - return false - } - return true - }) + return string(j, ctx) + .map(suitableOrigin, ctx) + .filter((s) => { + if (!ctx.vsv.aggregationCoordinatorOrigins.includes(s)) { + const allowed = ctx.vsv.aggregationCoordinatorOrigins.join(', ') + ctx.error(`must be one of the following: ${allowed}`) + return false + } + return true + }) } export type Trigger = CommonDebug & @@ -1560,13 +1476,13 @@ export type EventLevelReport = { } function reportDestination(j: Json, ctx: Context): Maybe { - const suitableSiteNoExtraneous = (j: Json) => - suitableSite(j, ctx, /*rejectExtraComponents=*/ true) + const suitableSiteNoExtraneous = (s: string) => + suitableSite(s, ctx, /*rejectExtraComponents=*/ true) return typeSwitch(j, ctx, { string: suitableSiteNoExtraneous, list: (j) => - array(j, ctx, suitableSiteNoExtraneous, { + array(j, ctx, (j) => string(j, ctx).map(suitableSiteNoExtraneous), { minLength: 2, maxLength: 3, }).filter((v) => { diff --git a/ts/src/header-validator/validate.ts b/ts/src/header-validator/validate.ts index 0ec96271ac..dce31130b8 100644 --- a/ts/src/header-validator/validate.ts +++ b/ts/src/header-validator/validate.ts @@ -1,3 +1,4 @@ +import * as psl from 'psl' import { Context, PathComponent } from './context' import { Maybe, Maybeable } from './maybe' @@ -280,3 +281,90 @@ export function clamp( } return n } + +export type LengthOpts = { + minLength?: number + maxLength?: number + maxLengthErrSuffix?: string +} + +export function isLengthValid( + length: number, + ctx: Context, + { + minLength = 0, + maxLength = Infinity, + maxLengthErrSuffix = '', + }: LengthOpts = {} +): boolean { + if (length > maxLength || length < minLength) { + ctx.error( + `length must be in the range [${minLength}, ${maxLength}${maxLengthErrSuffix}]` + ) + return false + } + return true +} + +function suitableScope( + s: string, + ctx: Context, + label: string, + scope: (url: URL) => string, + rejectExtraComponents: boolean +): Maybe { + let url + try { + url = new URL(s) + } catch { + ctx.error('invalid URL') + return Maybe.None + } + + if ( + url.protocol !== 'https:' && + !( + url.protocol === 'http:' && + (url.hostname === 'localhost' || url.hostname === '127.0.0.1') + ) + ) { + ctx.error('URL must use HTTP/HTTPS and be potentially trustworthy') + return Maybe.None + } + + const scoped = scope(url) + if (url.toString() !== new URL(scoped).toString()) { + if (rejectExtraComponents) { + ctx.error( + `must not contain URL components other than ${label} (${scoped})` + ) + return Maybe.None + } + ctx.warning( + `URL components other than ${label} (${scoped}) will be ignored` + ) + } + return Maybe.some(scoped) +} + +export function suitableOrigin( + s: string, + ctx: Context, + rejectExtraComponents: boolean = false +): Maybe { + return suitableScope(s, ctx, 'origin', (u) => u.origin, rejectExtraComponents) +} + +export function suitableSite( + s: string, + ctx: Context, + rejectExtraComponents: boolean = false +): Maybe { + return suitableScope( + s, + ctx, + 'site', + (u) => `${u.protocol}//${psl.get(u.hostname)}`, + rejectExtraComponents + ) +}