import * as plugins from '../plugins.js'; import { giteaRoutePathClassLabels, giteaRoutePathClassPatterns, routePathClasses, } from '../../ts_interfaces/data/route-management.js'; import type { IRoutePathPolicyBinding, IRouteMetadata, IRouteSecurity, IRouteSourceBinding, } from '../../ts_interfaces/data/route-management.js'; import type { ReferenceResolver } from './classes.reference-resolver.js'; const MIN_ROUTE_PRIORITY = 0; const MAX_ROUTE_PRIORITY = 10000; const SOURCE_PRIORITY_BAND = 0.0008; const PATH_PRIORITY_BAND = 0.0001; export const sourcePolicyLimits = { maxBindings: 16, maxPathPoliciesPerBinding: 12, maxPathPatternsPerPolicy: 64, maxPathPatternLength: 256, maxPathPatternWildcards: 8, maxSourceProfileRefLength: 256, maxIdLength: 128, maxExceededMessageLength: 512, maxCompiledVariantsPerRoute: 512, } as const; export class SourcePolicyCompiler { public static compileRoute( route: plugins.smartproxy.IRouteConfig, metadata: IRouteMetadata | undefined, referenceResolver: ReferenceResolver | undefined, routeId?: string, ): plugins.smartproxy.IRouteConfig[] { const bindings = metadata?.sourceBindings || []; if (bindings.length === 0) { return [route]; } if (this.validateSourceBindingsShape(bindings, route)) { return []; } if (!referenceResolver) { return []; } if (this.validateResolvedSourceBindings(bindings, referenceResolver)) { return []; } const compiledRoutes: plugins.smartproxy.IRouteConfig[] = []; const basePriority = route.priority ?? 0; let hasAllSourcesBinding = false; bindings.forEach((binding, index) => { const profile = referenceResolver.getProfile(binding.sourceProfileRef); const profileSecurity = referenceResolver.resolveSourceProfileSecurity(binding.sourceProfileRef); if (!profile || !profileSecurity) { return; } const sourceMatches = this.getSourceMatchEntries(profileSecurity); if (sourceMatches.length === 0) { return; } if (this.matchesAllSources(sourceMatches)) { hasAllSourcesBinding = true; } const sourcePriority = this.calculateSourcePriority(basePriority, index, bindings.length); const sourceMatch = this.matchesAllSources(sourceMatches) ? { ...route.match } : { ...route.match, clientIp: sourceMatches }; const pathPolicies = binding.pathPolicies || []; if (pathPolicies.length === 0) { compiledRoutes.push(this.buildCompiledRoute({ route, sourceMatch, profileName: profile.name, profileSecurity, binding, sourcePriority, routeId, sourceIndex: index, })); return; } let hasSourceFallback = false; pathPolicies.forEach((pathPolicy, pathIndex) => { const pathPatterns = this.getPathPatterns(pathPolicy); if (pathPatterns.length === 0) { hasSourceFallback = true; compiledRoutes.push(this.buildCompiledRoute({ route, sourceMatch, profileName: profile.name, profileSecurity, binding, pathPolicy, sourcePriority, routeId, sourceIndex: index, pathIndex, pathPolicyCount: pathPolicies.length, })); return; } pathPatterns.forEach((pathPattern, pathPatternIndex) => { compiledRoutes.push(this.buildCompiledRoute({ route, sourceMatch, profileName: profile.name, profileSecurity, binding, pathPolicy, pathPattern, sourcePriority, routeId, sourceIndex: index, pathIndex, pathPolicyCount: pathPolicies.length, pathPatternIndex, pathPatternCount: pathPatterns.length, })); }); }); if (!hasSourceFallback) { compiledRoutes.push(this.buildCompiledRoute({ route, sourceMatch, profileName: profile.name, profileSecurity, binding, sourcePriority, routeId, sourceIndex: index, })); } }); if (compiledRoutes.length > 0 && !hasAllSourcesBinding) { compiledRoutes.push(this.buildDenyFallbackRoute(route, basePriority, routeId)); } return this.applyIntegerPriorities(compiledRoutes, basePriority); } public static validateSourceBindingsPayload(sourceBindings?: Partial[]): string | undefined { if (sourceBindings === undefined) { return undefined; } if (!Array.isArray(sourceBindings)) { return 'Source bindings must be an array'; } if (sourceBindings.length === 0) { return undefined; } if (sourceBindings.length > sourcePolicyLimits.maxBindings) { return `Source policy exceeds ${sourcePolicyLimits.maxBindings} bindings`; } const validClasses = new Set(routePathClasses); for (const binding of sourceBindings) { if (!binding || typeof binding !== 'object') { return 'Source binding must be an object'; } if (typeof binding.sourceProfileRef !== 'string') { return 'Source binding requires a source profile'; } if (binding.sourceProfileRef.length > sourcePolicyLimits.maxSourceProfileRefLength) { return `Source binding source profile ref exceeds ${sourcePolicyLimits.maxSourceProfileRefLength} characters`; } if (binding.sourceProfileRef.trim().length === 0) { return 'Source binding requires a source profile'; } if (typeof binding.id === 'string' && binding.id.length > sourcePolicyLimits.maxIdLength) { return `Source binding id exceeds ${sourcePolicyLimits.maxIdLength} characters`; } if (typeof binding.maxConnections === 'number' && binding.maxConnections < 0) { return 'Source policy maxConnections must be non-negative'; } const bindingRateLimitError = this.validateRateLimitPayload(binding.rateLimit); if (bindingRateLimitError) { return bindingRateLimitError; } const bindingChallengeError = this.validateChallengePayload(binding.challenge); if (bindingChallengeError) { return bindingChallengeError; } const bindingMessage = binding.onExceeded?.errorMessage; if (typeof bindingMessage === 'string' && bindingMessage.length > sourcePolicyLimits.maxExceededMessageLength) { return `Source policy exceeded message exceeds ${sourcePolicyLimits.maxExceededMessageLength} characters`; } const pathPolicies = binding.pathPolicies; if (pathPolicies === undefined) { continue; } if (!Array.isArray(pathPolicies)) { return 'Source policy path policies must be an array'; } if (pathPolicies.length > sourcePolicyLimits.maxPathPoliciesPerBinding) { return `Source policy binding exceeds ${sourcePolicyLimits.maxPathPoliciesPerBinding} path policies`; } for (const pathPolicy of pathPolicies) { if (!pathPolicy || typeof pathPolicy !== 'object') { return 'Source policy path policy must be an object'; } if (!validClasses.has(pathPolicy.pathClass)) { return 'Source policy path policy uses an unsupported path class'; } if (typeof pathPolicy.id === 'string' && pathPolicy.id.length > sourcePolicyLimits.maxIdLength) { return `Source policy path policy id exceeds ${sourcePolicyLimits.maxIdLength} characters`; } if (typeof pathPolicy.maxConnections === 'number' && pathPolicy.maxConnections < 0) { return 'Source policy path policy maxConnections must be non-negative'; } const pathRateLimitError = this.validateRateLimitPayload(pathPolicy.rateLimit); if (pathRateLimitError) { return pathRateLimitError; } const pathChallengeError = this.validateChallengePayload(pathPolicy.challenge); if (pathChallengeError) { return pathChallengeError; } const pathMessage = pathPolicy.onExceeded?.errorMessage; if (typeof pathMessage === 'string' && pathMessage.length > sourcePolicyLimits.maxExceededMessageLength) { return `Source policy exceeded message exceeds ${sourcePolicyLimits.maxExceededMessageLength} characters`; } const pathPatterns = pathPolicy.pathPatterns; if (pathPatterns === undefined) { continue; } if (!Array.isArray(pathPatterns)) { return 'Source policy path patterns must be an array'; } if (pathPatterns.length > sourcePolicyLimits.maxPathPatternsPerPolicy) { return `Source policy path class exceeds ${sourcePolicyLimits.maxPathPatternsPerPolicy} path patterns`; } for (const pattern of pathPatterns) { if (typeof pattern !== 'string') { return 'Source policy path pattern must be a string'; } if (pattern.length > sourcePolicyLimits.maxPathPatternLength) { return `Source policy path pattern exceeds ${sourcePolicyLimits.maxPathPatternLength} characters`; } const wildcardCount = pattern.split('*').length - 1; if (wildcardCount > sourcePolicyLimits.maxPathPatternWildcards) { return `Source policy path pattern exceeds ${sourcePolicyLimits.maxPathPatternWildcards} wildcards`; } } } } return undefined; } private static validateRateLimitPayload(rateLimit: IRouteSecurity['rateLimit'] | undefined): string | undefined { if (rateLimit === null || rateLimit === undefined) { return undefined; } if (typeof rateLimit !== 'object') { return 'Source policy rate limit must be an object, null, or omitted'; } const rawRateLimit = rateLimit as unknown as Record; for (const key of ['maxRequests', 'window'] as const) { const value = rawRateLimit[key]; if (typeof value === 'string' && value.length > 32) { return `Source policy rate limit ${key} exceeds 32 characters`; } } if ( typeof rateLimit.errorMessage === 'string' && rateLimit.errorMessage.length > sourcePolicyLimits.maxExceededMessageLength ) { return `Source policy rate limit error message exceeds ${sourcePolicyLimits.maxExceededMessageLength} characters`; } const rawOnExceeded = rawRateLimit.onExceeded; if (rawOnExceeded !== undefined) { if (!rawOnExceeded || typeof rawOnExceeded !== 'object') { return 'Source policy rate limit onExceeded must be an object or omitted'; } const onExceeded = rawOnExceeded as NonNullable['onExceeded']; if (!onExceeded || !['429', 'challenge'].includes(onExceeded.type)) { return 'Source policy rate limit onExceeded.type must be 429 or challenge'; } if ( onExceeded.clearanceEffect !== undefined && !['bypass-rate-limit', 'none'].includes(onExceeded.clearanceEffect) ) { return 'Source policy rate limit onExceeded.clearanceEffect must be bypass-rate-limit or none'; } if (onExceeded.type === 'challenge') { if (!onExceeded.challenge) { return 'Source policy rate limit challenge requires challenge config'; } const challengeError = this.validateChallengePayload(onExceeded.challenge); if (challengeError) { return challengeError; } } } return undefined; } private static validateChallengePayload(challenge: IRouteSecurity['challenge'] | undefined): string | undefined { if (challenge === null || challenge === undefined) { return undefined; } if (typeof challenge !== 'object') { return 'Source policy challenge must be an object, null, or omitted'; } if (typeof challenge.providerId !== 'string' || challenge.providerId.trim().length === 0) { return 'Source policy challenge requires providerId'; } if (typeof challenge.challengeType !== 'string' || challenge.challengeType.trim().length === 0) { return 'Source policy challenge requires challengeType'; } if (challenge.providerId.length > sourcePolicyLimits.maxIdLength) { return `Source policy challenge providerId exceeds ${sourcePolicyLimits.maxIdLength} characters`; } if (challenge.challengeType.length > sourcePolicyLimits.maxIdLength) { return `Source policy challenge challengeType exceeds ${sourcePolicyLimits.maxIdLength} characters`; } return undefined; } public static validateSourcePolicyShape( sourceBindings?: IRouteSourceBinding[], route?: plugins.smartproxy.IRouteConfig, ): string | undefined { return this.validateSourceBindingsShape(sourceBindings, route); } public static validateSourceBindingsShape( sourceBindings?: IRouteSourceBinding[], route?: plugins.smartproxy.IRouteConfig, ): string | undefined { const payloadError = this.validateSourceBindingsPayload(sourceBindings); if (payloadError) { return payloadError; } const bindings = sourceBindings || []; if (bindings.length === 0) { return undefined; } let estimatedCompiledRoutes = 0; for (const binding of bindings) { const pathPolicies = binding.pathPolicies || []; if (pathPolicies.length === 0) { estimatedCompiledRoutes++; } else { let hasSourceFallback = false; for (const pathPolicy of pathPolicies) { const pathPatterns = this.getPathPatterns(pathPolicy); if (pathPatterns.length > sourcePolicyLimits.maxPathPatternsPerPolicy) { return `Source policy path class expands beyond ${sourcePolicyLimits.maxPathPatternsPerPolicy} path patterns`; } if (pathPatterns.length === 0) { hasSourceFallback = true; estimatedCompiledRoutes++; } else { estimatedCompiledRoutes += pathPatterns.length; } } if (!hasSourceFallback) { estimatedCompiledRoutes++; } } if (estimatedCompiledRoutes > sourcePolicyLimits.maxCompiledVariantsPerRoute) { return `Source policy exceeds ${sourcePolicyLimits.maxCompiledVariantsPerRoute} compiled route variants`; } } // Private-only source bindings add one terminal deny route to prevent fall-through // to broader routes with the same host/path/port scope. estimatedCompiledRoutes++; const expandedPortCount = route ? this.getExpandedPortCount(route.match?.ports) : 1; if (estimatedCompiledRoutes * expandedPortCount > sourcePolicyLimits.maxCompiledVariantsPerRoute) { return `Source policy exceeds ${sourcePolicyLimits.maxCompiledVariantsPerRoute} compiled route-port variants`; } if (route && typeof route.priority === 'number' && Number.isFinite(route.priority)) { const integerBasePriority = Math.trunc(this.clampPriority(route.priority)); if (integerBasePriority + estimatedCompiledRoutes > MAX_ROUTE_PRIORITY) { return `Source policy route priority leaves no priority headroom for ${estimatedCompiledRoutes} compiled variants`; } } return undefined; } public static validateResolvedSourcePolicy( sourceBindings: IRouteSourceBinding[] | undefined, referenceResolver: ReferenceResolver | undefined, ): string | undefined { return this.validateResolvedSourceBindings(sourceBindings, referenceResolver); } public static validateResolvedSourceBindings( sourceBindings: IRouteSourceBinding[] | undefined, referenceResolver: ReferenceResolver | undefined, ): string | undefined { const bindings = sourceBindings || []; if (bindings.length === 0) { return undefined; } if (!referenceResolver) { return 'Source policy requires source profile resolution'; } for (let index = 0; index < bindings.length; index++) { const binding = bindings[index]; const profile = referenceResolver.getProfile(binding.sourceProfileRef); if (!profile) { return `Source profile '${binding.sourceProfileRef}' not found`; } const profileSecurity = referenceResolver.resolveSourceProfileSecurity(binding.sourceProfileRef); if (!profileSecurity) { return `Source profile '${profile.name}' could not be resolved`; } const sourceMatches = this.getSourceMatchEntries(profileSecurity); if (sourceMatches.length === 0) { return `Source profile '${profile.name}' has no source matches`; } const matchesAllSources = this.matchesAllSources(sourceMatches); if (matchesAllSources && index < bindings.length - 1) { return 'Wildcard source profile bindings must be last in source bindings'; } } return undefined; } private static buildCompiledRoute(options: { route: plugins.smartproxy.IRouteConfig; sourceMatch: plugins.smartproxy.IRouteConfig['match']; profileName: string; profileSecurity: IRouteSecurity; binding: IRouteSourceBinding; pathPolicy?: IRoutePathPolicyBinding; pathPattern?: string; sourcePriority: number; routeId?: string; sourceIndex: number; pathIndex?: number; pathPolicyCount?: number; pathPatternIndex?: number; pathPatternCount?: number; }): plugins.smartproxy.IRouteConfig { const routeKey = options.route.id || options.routeId || options.route.name || 'route'; const bindingKey = options.binding.id || options.binding.sourceProfileRef || String(options.sourceIndex + 1); const pathPolicyKey = options.pathPolicy ? options.pathPolicy.id || options.pathPolicy.pathClass : undefined; const pathLabel = options.pathPolicy ? giteaRoutePathClassLabels[options.pathPolicy.pathClass] : undefined; const pathPatternSuffix = options.pathPatternCount && options.pathPatternCount > 1 ? `:${(options.pathPatternIndex || 0) + 1}` : ''; const pathPriority = options.pathPolicy ? this.calculatePathPriorityOffset( options.pathPattern, options.pathIndex || 0, options.pathPolicyCount || 1, options.pathPatternIndex || 0, options.pathPatternCount || 1, ) : 0; const security = this.buildBindingSecurity( options.route.security, options.profileSecurity, options.binding, options.pathPolicy, ); const match: plugins.smartproxy.IRouteConfig['match'] = options.pathPattern ? { ...options.sourceMatch, path: options.pathPattern } : { ...options.sourceMatch }; if (this.requiresHttpProtocol(security)) { match.protocol = 'http'; } return { ...options.route, id: pathPolicyKey ? `${routeKey}:source:${bindingKey}:path:${pathPolicyKey}${pathPatternSuffix}` : `${routeKey}:source:${bindingKey}`, name: pathLabel ? `${options.route.name || routeKey}:source:${options.profileName}:path:${pathLabel}${pathPatternSuffix}` : `${options.route.name || routeKey}:source:${options.profileName}`, match, priority: this.clampPriority(options.sourcePriority + pathPriority), security, }; } private static buildDenyFallbackRoute( route: plugins.smartproxy.IRouteConfig, basePriority: number, routeId?: string, ): plugins.smartproxy.IRouteConfig { const routeKey = route.id || routeId || route.name || 'route'; return { ...route, id: `${routeKey}:source:deny-fallback`, name: `${route.name || routeKey}:source:deny-fallback`, match: { ...route.match }, priority: this.clampPriority(basePriority - SOURCE_PRIORITY_BAND - PATH_PRIORITY_BAND), action: { type: 'socket-handler', socketHandler: (socket) => this.denySocket(socket), }, security: undefined, }; } private static denySocket(socket: plugins.net.Socket): void { let timeout: ReturnType & { unref?: () => void }; const cleanup = () => { clearTimeout(timeout); socket.removeListener('data', handleData); socket.removeListener('error', cleanup); socket.removeListener('close', cleanup); }; const handleData = (chunk: string | Uint8Array) => { cleanup(); if (this.looksLikeHttpRequest(chunk)) { socket.end('HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: close\r\n\r\nForbidden'); return; } socket.destroy(); }; timeout = setTimeout(() => { cleanup(); socket.destroy(); }, 2000) as ReturnType & { unref?: () => void }; timeout.unref?.(); socket.once('data', handleData); socket.once('error', cleanup); socket.once('close', cleanup); } private static looksLikeHttpRequest(chunk: string | Uint8Array): boolean { const prefix = typeof chunk === 'string' ? chunk.slice(0, 16) : String.fromCharCode(...chunk.subarray(0, 16)); return /^(GET|POST|HEAD|PUT|PATCH|DELETE|OPTIONS|TRACE|CONNECT)\s/.test(prefix) || prefix.startsWith('PRI * HTTP/2.0'); } private static getPathPatterns(pathPolicy: IRoutePathPolicyBinding): string[] { const patterns: string[] = pathPolicy.pathPatterns?.length ? pathPolicy.pathPatterns : giteaRoutePathClassPatterns[pathPolicy.pathClass]; return [...new Set(patterns.map((pattern) => pattern.trim()).filter(Boolean))]; } private static calculatePathPriorityOffset( pathPattern: string | undefined, pathIndex: number, pathPolicyCount: number, pathPatternIndex: number, pathPatternCount: number, ): number { if (!pathPattern) { return 0; } const pathPolicyOffset = ((pathPolicyCount - pathIndex) / (pathPolicyCount + 1)) * (PATH_PRIORITY_BAND * 0.9); const pathPatternOffset = ((pathPatternCount - pathPatternIndex) / (pathPatternCount + 1)) * (PATH_PRIORITY_BAND * 0.1 / (pathPolicyCount + 1)); return pathPolicyOffset + pathPatternOffset; } private static calculateSourcePriority( basePriority: number, sourceIndex: number, sourceCount: number, ): number { const safeBasePriority = this.clampPriority( basePriority, MIN_ROUTE_PRIORITY, MAX_ROUTE_PRIORITY - SOURCE_PRIORITY_BAND - PATH_PRIORITY_BAND, ); const sourceStep = SOURCE_PRIORITY_BAND / (sourceCount + 1); return safeBasePriority + ((sourceCount - sourceIndex) * sourceStep); } private static applyIntegerPriorities( routes: plugins.smartproxy.IRouteConfig[], basePriority: number, ): plugins.smartproxy.IRouteConfig[] { if (routes.length === 0) { return routes; } const priorityOrder = routes .map((route, originalIndex) => ({ originalIndex, priority: typeof route.priority === 'number' && Number.isFinite(route.priority) ? route.priority : basePriority, })) .sort((a, b) => (b.priority - a.priority) || (a.originalIndex - b.originalIndex)); const topPriority = Math.trunc(this.clampPriority( basePriority + routes.length, MIN_ROUTE_PRIORITY + routes.length, MAX_ROUTE_PRIORITY, )); const integerPriorities = new Map(); priorityOrder.forEach((entry, index) => { integerPriorities.set(entry.originalIndex, topPriority - index); }); return routes.map((route, index) => ({ ...route, priority: integerPriorities.get(index) ?? MIN_ROUTE_PRIORITY, })); } private static clampPriority( priority: number, min = MIN_ROUTE_PRIORITY, max = MAX_ROUTE_PRIORITY, ): number { if (!Number.isFinite(priority)) { return min; } return Math.min(max, Math.max(min, priority)); } private static getExpandedPortCount(portRange: plugins.smartproxy.IRouteConfig['match']['ports'] | undefined): number { if (portRange === undefined) { return 1; } if (typeof portRange === 'number') { return Number.isFinite(portRange) ? 1 : sourcePolicyLimits.maxCompiledVariantsPerRoute + 1; } if (!Array.isArray(portRange)) { return sourcePolicyLimits.maxCompiledVariantsPerRoute + 1; } let count = 0; for (const portEntry of portRange) { if (typeof portEntry === 'number') { if (!Number.isFinite(portEntry)) { return sourcePolicyLimits.maxCompiledVariantsPerRoute + 1; } count++; } else if ( portEntry && typeof portEntry === 'object' && Number.isFinite(portEntry.from) && Number.isFinite(portEntry.to) && portEntry.from <= portEntry.to ) { count += Math.floor(portEntry.to) - Math.floor(portEntry.from) + 1; } else { return sourcePolicyLimits.maxCompiledVariantsPerRoute + 1; } if (count > sourcePolicyLimits.maxCompiledVariantsPerRoute) { return count; } } return Math.max(1, count); } private static normalizeMaxConnections(value: IRouteSecurity['maxConnections']): number | undefined { return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined; } private static forceIpRateLimit( rateLimit: IRouteSecurity['rateLimit'] | undefined, ): IRouteSecurity['rateLimit'] | undefined { if (rateLimit === null) { return null; } if (!rateLimit || rateLimit.enabled === false) { return undefined; } const { headerName: _headerName, ...rest } = structuredClone(rateLimit); if (Number(rest.maxRequests) <= 0 || Number(rest.window) <= 0) { return undefined; } return { ...rest, enabled: true, keyBy: 'ip', } as IRouteSecurity['rateLimit']; } private static sanitizeSourcePolicySecurity( security: IRouteSecurity, options: { preserveClears?: boolean } = {}, ): IRouteSecurity { const sanitized = structuredClone(security); const maxConnections = this.normalizeMaxConnections(sanitized.maxConnections); if (maxConnections === undefined) { delete sanitized.maxConnections; } else { sanitized.maxConnections = maxConnections; } if (sanitized.rateLimit !== undefined && sanitized.rateLimit !== null) { const rateLimit = this.forceIpRateLimit(sanitized.rateLimit); if (rateLimit === undefined) { delete sanitized.rateLimit; } else { sanitized.rateLimit = rateLimit; } } if (sanitized.rateLimit === null && !options.preserveClears) { delete sanitized.rateLimit; } if (sanitized.challenge === null && !options.preserveClears) { delete sanitized.challenge; } return sanitized; } private static isEmptySecurity(security: IRouteSecurity): boolean { return Object.keys(security).length === 0; } private static getSourceMatchEntries(security: IRouteSecurity): string[] { const entries = security.ipAllowList || []; const normalizedEntries: string[] = []; for (const entry of entries) { const rawEntry = typeof entry === 'string' ? entry : entry.ip; if (typeof rawEntry !== 'string') continue; const normalizedEntry = rawEntry.trim(); if (normalizedEntry) { normalizedEntries.push(normalizedEntry); } } return [...new Set(normalizedEntries)]; } private static matchesAllSources(sourceMatches: string[]): boolean { return sourceMatches.includes('*') || (sourceMatches.includes('0.0.0.0/0') && sourceMatches.includes('::/0')); } private static buildBindingSecurity( routeSecurity: IRouteSecurity | undefined, profileSecurity: IRouteSecurity, binding: IRouteSourceBinding, pathPolicy?: IRoutePathPolicyBinding, ): plugins.smartproxy.IRouteConfig['security'] { const baseSecurity = this.omitSourceMatchFields(routeSecurity || {}); delete baseSecurity.rateLimit; delete baseSecurity.challenge; const sourceSecurity = this.omitSourceMatchFields(profileSecurity, { preserveClears: true }); if (binding.rateLimit !== undefined) { const rateLimit = this.forceIpRateLimit(binding.rateLimit); if (rateLimit !== undefined) { sourceSecurity.rateLimit = rateLimit; } } if (binding.challenge !== undefined) { sourceSecurity.challenge = binding.challenge; } if (binding.maxConnections !== undefined) { const maxConnections = this.normalizeMaxConnections(binding.maxConnections); if (maxConnections === undefined) { delete sourceSecurity.maxConnections; } else { sourceSecurity.maxConnections = maxConnections; } } if (binding.onExceeded?.errorMessage && sourceSecurity.rateLimit) { sourceSecurity.rateLimit = { ...sourceSecurity.rateLimit, errorMessage: binding.onExceeded.errorMessage, }; } if (pathPolicy?.rateLimit !== undefined) { const rateLimit = this.forceIpRateLimit(pathPolicy.rateLimit); if (rateLimit !== undefined) { sourceSecurity.rateLimit = rateLimit; } } if (pathPolicy?.challenge !== undefined) { sourceSecurity.challenge = pathPolicy.challenge; } if (pathPolicy?.maxConnections !== undefined) { const maxConnections = this.normalizeMaxConnections(pathPolicy.maxConnections); if (maxConnections === undefined) { delete sourceSecurity.maxConnections; } else { sourceSecurity.maxConnections = maxConnections; } } if (pathPolicy?.onExceeded?.errorMessage && sourceSecurity.rateLimit) { sourceSecurity.rateLimit = { ...sourceSecurity.rateLimit, errorMessage: pathPolicy.onExceeded.errorMessage, }; } const mergedSecurity = this.sanitizeSourcePolicySecurity({ ...baseSecurity, ...sourceSecurity, }); if (this.isEmptySecurity(mergedSecurity)) { return undefined; } const { rateLimit, challenge, ...rest } = mergedSecurity; return { ...rest, ...(rateLimit ? { rateLimit } : {}), ...(challenge ? { challenge } : {}), }; } private static requiresHttpProtocol(security: IRouteSecurity | undefined): boolean { return Boolean( security?.challenge || (security?.rateLimit && security.rateLimit !== null && security.rateLimit.onExceeded?.type === 'challenge'), ); } private static omitSourceMatchFields( security: IRouteSecurity, options: { preserveClears?: boolean } = {}, ): IRouteSecurity { const { ipAllowList: _ipAllowList, ...controls } = security; return this.sanitizeSourcePolicySecurity(controls, options); } }