@bitblit/ratchet-epsilon-common
Version:
Tiny adapter to simplify building API gateway Lambda APIS
244 lines • 11.7 kB
JavaScript
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