UNPKG

@bitblit/ratchet-epsilon-common

Version:

Tiny adapter to simplify building API gateway Lambda APIS

244 lines 11.7 kB
import { Logger } from '@bitblit/ratchet-common/logger/logger'; import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet'; import { RestfulApiHttpError } from '@bitblit/ratchet-common/network/restful-api-http-error'; import { MapRatchet } from '@bitblit/ratchet-common/lang/map-ratchet'; import { EventUtil } from '../../http/event-util.js'; import { BadRequestError } from '../../http/error/bad-request-error.js'; import { ResponseUtil } from '../../http/response-util.js'; import { MisconfiguredError } from '../../http/error/misconfigured-error.js'; import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet'; import { EpsilonCorsApproach } from '../../config/http/epsilon-cors-approach.js'; export class BuiltInFilters { static MAXIMUM_LAMBDA_BODY_SIZE_BYTES = 1024 * 1024 * 5 - 1024 * 100; static async combineFilters(fCtx, filters) { let cont = true; if (filters && filters.length > 0) { for (let i = 0; i < filters.length && cont; i++) { cont = await filters[i](fCtx); } } return cont; } static async applyGzipIfPossible(fCtx) { if (fCtx.event?.headers && fCtx.result) { const encodingHeader = fCtx.event && fCtx.event.headers ? MapRatchet.extractValueFromMapIgnoreCase(fCtx.event.headers, 'accept-encoding') : null; fCtx.result = await ResponseUtil.applyGzipIfPossible(encodingHeader, fCtx.result); } return true; } static async addConstantHeaders(fCtx, headers) { if (headers && fCtx.result) { fCtx.result.headers = Object.assign({}, headers, fCtx.result.headers); } else { Logger.warn('Could not add headers - either result or headers were missing'); } return true; } static async addAWSRequestIdHeader(fCtx, headerName = 'X-REQUEST-ID') { if (fCtx.result && StringRatchet.trimToNull(headerName) && headerName.startsWith('X-')) { fCtx.result.headers = fCtx.result.headers || {}; fCtx.result.headers[headerName] = fCtx.context?.awsRequestId || 'Request-Id-Missing'; } else { Logger.warn('Could not add request id header - either result or context were missing or name was invalid'); } return true; } static async addAllowEverythingCORSHeaders(fCtx) { return BuiltInFilters.addConstantHeaders(fCtx, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers': '*', }); } static async addAllowReflectionCORSHeaders(fCtx) { return BuiltInFilters.addConstantHeaders(fCtx, { 'Access-Control-Allow-Origin': MapRatchet.caseInsensitiveAccess(fCtx.event.headers, 'Origin') || '*', 'Access-Control-Allow-Methods': MapRatchet.caseInsensitiveAccess(fCtx.event.headers, 'Access-Control-Request-Method') || '*', 'Access-Control-Allow-Headers': MapRatchet.caseInsensitiveAccess(fCtx.event.headers, 'Access-Control-Request-Headers') || '*', }); } static async uriDecodeQueryParams(fCtx) { if (fCtx?.event?.queryStringParameters) { Object.keys(fCtx.event.queryStringParameters).forEach((k) => { const val = fCtx.event.queryStringParameters[k]; if (val) { fCtx.event.queryStringParameters[k] = BuiltInFilters.decodeUriComponentAndReplacePlus(val); } }); } if (fCtx?.event?.multiValueQueryStringParameters) { Object.keys(fCtx.event.multiValueQueryStringParameters).forEach((k) => { const val = fCtx.event.multiValueQueryStringParameters[k]; if (val && val.length) { const cleaned = val.map((v) => BuiltInFilters.decodeUriComponentAndReplacePlus(v)); fCtx.event.multiValueQueryStringParameters[k] = cleaned; } }); } return true; } static decodeUriComponentAndReplacePlus(val) { return ResponseUtil.decodeUriComponentAndReplacePlus(val); } static async fixStillEncodedQueryParams(fCtx) { EventUtil.fixStillEncodedQueryParams(fCtx.event); return true; } static createRestrictServerToHostNamesFilter(hostnameRegExList) { RequireRatchet.notNullUndefinedOrEmptyArray(hostnameRegExList, 'hostnameRegExList'); return async (fCtx) => { const hostName = StringRatchet.trimToNull(MapRatchet.extractValueFromMapIgnoreCase(fCtx?.event?.headers, 'host')); if (!StringRatchet.trimToNull(hostName)) { throw new BadRequestError('No host name found in headers : ' + JSON.stringify(fCtx?.event?.headers)); } const hostMatches = EventUtil.hostMatchesRegexInList(hostName, hostnameRegExList); if (!hostMatches) { throw new BadRequestError('Host does not match list : ' + hostName + ' :: ' + hostnameRegExList); } return true; }; } static async disallowStringNullAsPathParameter(fCtx) { if (fCtx?.event?.pathParameters) { Object.keys(fCtx.event.pathParameters).forEach((k) => { if ('null' === StringRatchet.trimToEmpty(fCtx.event.pathParameters[k]).toLowerCase()) { throw new BadRequestError().withFormattedErrorMessage('Path parameter %s was string -null-', k); } }); } return true; } static async disallowStringNullAsQueryStringParameter(fCtx) { if (fCtx?.event?.queryStringParameters) { Object.keys(fCtx.event.queryStringParameters).forEach((k) => { if ('null' === StringRatchet.trimToEmpty(fCtx.event.queryStringParameters[k]).toLowerCase()) { throw new BadRequestError().withFormattedErrorMessage('Query parameter %s was string -null-', k); } }); } return true; } static async ensureEventMaps(fCtx) { fCtx.event.queryStringParameters = fCtx.event.queryStringParameters || {}; fCtx.event.headers = fCtx.event.headers || {}; fCtx.event.pathParameters = fCtx.event.pathParameters || {}; return true; } static async parseJsonBodyToObject(fCtx) { if (fCtx.event?.body) { try { fCtx.event.parsedBody = EventUtil.jsonBodyToObject(fCtx.event); } catch (err) { throw new RestfulApiHttpError('Supplied body was not parsable as valid JSON').withHttpStatusCode(400).withWrappedError(err); } } return true; } static async checkMaximumLambdaBodySize(fCtx) { if (fCtx.result?.body && fCtx.result.body.length > BuiltInFilters.MAXIMUM_LAMBDA_BODY_SIZE_BYTES) { const delta = fCtx.result.body.length - BuiltInFilters.MAXIMUM_LAMBDA_BODY_SIZE_BYTES; throw new RestfulApiHttpError('Response size is ' + fCtx.result.body.length + ' bytes, which is ' + delta + ' bytes too large for this handler').withHttpStatusCode(500); } return true; } static async validateInboundBody(fCtx) { if (fCtx?.event?.parsedBody && fCtx.routeAndParse) { if (fCtx.routeAndParse.mapping.validation) { if (!fCtx.modelValidator) { throw new MisconfiguredError('Requested body validation but supplied no validator'); } const errors = fCtx.modelValidator.validate(fCtx.routeAndParse.mapping.validation.modelName, fCtx.event.parsedBody, fCtx.routeAndParse.mapping.validation.emptyAllowed, fCtx.routeAndParse.mapping.validation.extraPropertiesAllowed); if (errors.length > 0) { Logger.info('Found errors while validating %s object %j', fCtx.routeAndParse.mapping.validation.modelName, errors); const newError = new BadRequestError(...errors); throw newError; } } } else { Logger.debug('No validation since no route specified or no parsed body'); } return true; } static async validateInboundQueryParams(_fCtx) { return true; } static async validateInboundPathParams(_fCtx) { return true; } static async validateOutboundResponse(fCtx) { if (fCtx?.rawResult) { if (fCtx.routeAndParse.mapping.outboundValidation) { Logger.debug('Applying outbound check to %j', fCtx.rawResult); const errors = fCtx.modelValidator.validate(fCtx.routeAndParse.mapping.outboundValidation.modelName, fCtx.rawResult, fCtx.routeAndParse.mapping.outboundValidation.emptyAllowed, fCtx.routeAndParse.mapping.outboundValidation.extraPropertiesAllowed); if (errors.length > 0) { Logger.error('Found outbound errors while validating %s object %j', fCtx.routeAndParse.mapping.outboundValidation.modelName, errors); errors.unshift('Server sent object invalid according to spec'); throw new RestfulApiHttpError().withErrors(errors).withHttpStatusCode(500).withDetails(fCtx.rawResult); } } else { Logger.debug('Applied no outbound validation because none set'); } } else { Logger.debug('No validation since no outbound body or disabled'); } return true; } static async autoRespondToOptionsRequestWithCors(fCtx, corsMethod = EpsilonCorsApproach.Reflective) { if (StringRatchet.trimToEmpty(fCtx?.event?.httpMethod).toLowerCase() === 'options') { fCtx.result = { statusCode: 200, body: '{"cors":true, "m":3}', }; await BuiltInFilters.addCorsHeadersDynamically(fCtx, corsMethod); return false; } else { return true; } } static async autoRespond(fCtx, inBody) { const body = inBody || { message: 'Not Implemented', }; fCtx.result = { statusCode: 200, body: JSON.stringify(body), }; return false; } static async secureOutboundServerErrorForProduction(fCtx, errorMessage, errCode) { if (fCtx?.result?.statusCode) { if (errCode === null || fCtx.result.statusCode === errCode) { Logger.warn('Securing outbound error info (was : %j)', fCtx.result.body); fCtx.rawResult = new RestfulApiHttpError(errorMessage).withHttpStatusCode(fCtx.result.statusCode); const oldResult = fCtx.result; fCtx.result = ResponseUtil.errorResponse(fCtx.rawResult); fCtx.result.headers = Object.assign({}, oldResult.headers || {}, fCtx.result.headers || {}); } } return true; } static async addCorsHeadersDynamically(fCtx, corsMethod) { if (corsMethod) { switch (corsMethod) { case EpsilonCorsApproach.All: await BuiltInFilters.addAllowEverythingCORSHeaders(fCtx); break; case EpsilonCorsApproach.Reflective: await BuiltInFilters.addAllowReflectionCORSHeaders(fCtx); break; default: } } else { Logger.warn('Called add CORS headers dynamically but no type supplied, using NONE'); } } } //# sourceMappingURL=built-in-filters.js.map