UNPKG

@bitblit/ratchet-epsilon-common

Version:

Tiny adapter to simplify building API gateway Lambda APIS

206 lines 9.27 kB
import { UnauthorizedError } from './error/unauthorized-error.js'; import { BadRequestError } from './error/bad-request-error.js'; import { Logger } from '@bitblit/ratchet-common/logger/logger'; import jwt from 'jsonwebtoken'; import { EpsilonConstants } from '../epsilon-constants.js'; import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet'; import { LoggerLevelName } from '@bitblit/ratchet-common/logger/logger-level-name'; import { Base64Ratchet } from '@bitblit/ratchet-common/lang/base64-ratchet'; import { MapRatchet } from '@bitblit/ratchet-common/lang/map-ratchet'; import { EnumRatchet } from '@bitblit/ratchet-common/lang/enum-ratchet'; export class EventUtil { static LOCAL_REGEX = [new RegExp('^127\\.0\\.0\\.1(:\\d+)?$'), new RegExp('^localhost(:\\d+)?$')]; static NON_ROUTABLE_REGEX = EventUtil.LOCAL_REGEX.concat([ new RegExp('^192\\.168\\.\\d+\\.\\d+(:\\d+)?$'), new RegExp('^10\\.\\d+\\.\\d+\\.\\d+(:\\d+)?$'), new RegExp('^172\\.16\\.\\d+\\.\\d+(:\\d+)?$'), ]); constructor() { } static extractStage(event) { if (!event.path.startsWith('/')) { throw new BadRequestError('Path should start with / but does not : ' + event.path); } const idx = event.path.indexOf('/', 1); if (idx == -1) { throw new BadRequestError('No second / found in the path : ' + event.path); } return event.path.substring(1, idx); } static extractHostHeader(event) { return MapRatchet.extractValueFromMapIgnoreCase(event.headers, 'Host'); } static extractProtocol(event) { return MapRatchet.extractValueFromMapIgnoreCase(event.headers, 'X-Forwarded-Proto'); } static extractApiGatewayStage(event) { const rc = EventUtil.extractRequestContext(event); return rc ? rc.stage : null; } static extractRequestContext(event) { return event.requestContext; } static extractAuthorizer(event) { const rc = EventUtil.extractRequestContext(event); return rc ? rc.authorizer : null; } static ipAddressChain(event) { const headerVal = event && event.headers ? MapRatchet.extractValueFromMapIgnoreCase(event.headers, 'X-Forwarded-For') : null; let headerList = headerVal ? String(headerVal).split(',') : []; headerList = headerList.map((s) => s.trim()); return headerList; } static ipAddress(event) { const list = EventUtil.ipAddressChain(event); return list && list.length > 0 ? list[0] : null; } static extractFullPath(event, overrideProtocol = null) { const protocol = overrideProtocol || EventUtil.extractProtocol(event) || 'https'; return protocol + '://' + event.requestContext['domainName'] + event.requestContext.path; } static extractFullPrefix(event, overrideProtocol = null) { const protocol = overrideProtocol || EventUtil.extractProtocol(event) || 'https'; const prefix = event.requestContext.path.substring(0, event.requestContext.path.indexOf('/', 1)); return protocol + '://' + event.requestContext['domainName'] + prefix; } static jsonBodyToObject(evt) { let rval = null; if (evt.body) { const contentType = MapRatchet.extractValueFromMapIgnoreCase(evt.headers, 'Content-Type') || 'application/octet-stream'; rval = evt.body; if (evt.isBase64Encoded) { rval = Base64Ratchet.base64StringToString(rval); } if (contentType.startsWith('application/json')) { rval = JSON.parse(rval.toString('ascii')); } } return rval; } static calcLogLevelViaEventOrEnvParam(curLevel, event, rConfig) { let rval = curLevel; if (rConfig?.envParamLogLevelName && process.env[rConfig.envParamLogLevelName]) { rval = EnumRatchet.keyToEnum(LoggerLevelName, process.env[rConfig.envParamLogLevelName]); Logger.silly('Found env log level : %s', rval); } if (rConfig && rConfig.queryParamLogLevelName && event && event.queryStringParameters && event.queryStringParameters[rConfig.queryParamLogLevelName]) { rval = EnumRatchet.keyToEnum(LoggerLevelName, event.queryStringParameters[rConfig.queryParamLogLevelName]); Logger.silly('Found query log level : %s', rval); } return rval; } static fixStillEncodedQueryParams(event) { if (event?.queryStringParameters) { const newParams = {}; Object.keys(event.queryStringParameters).forEach((k) => { const val = event.queryStringParameters[k]; if (k.toLowerCase().startsWith('amp;')) { newParams[k.substring(4)] = val; } else { newParams[k] = val; } }); event.queryStringParameters = newParams; } if (event?.multiValueQueryStringParameters) { const newParams = {}; Object.keys(event.multiValueQueryStringParameters).forEach((k) => { const val = event.multiValueQueryStringParameters[k]; if (k.toLowerCase().startsWith('amp;')) { newParams[k.substring(4)] = val; } else { newParams[k] = val; } }); event.multiValueQueryStringParameters = newParams; } } static applyTokenToEventForTesting(event, jwtToken) { const jwtFullData = jwt.decode(jwtToken, { complete: true }); if (!jwtFullData['payload']) { throw new Error('No payload found in passed token'); } const jwtData = jwtFullData['payload']; event.headers = event.headers || {}; event.headers[EpsilonConstants.AUTH_HEADER_NAME.toLowerCase()] = 'Bearer ' + jwtToken; event.requestContext = event.requestContext || {}; const newAuth = Object.assign({}, event.requestContext.authorizer); newAuth.userData = jwtData; newAuth.userDataJSON = jwtData ? JSON.stringify(jwtData) : null; newAuth.srcData = jwtToken; event.requestContext.authorizer = newAuth; } static extractBasicAuthenticationToken(event, throwErrorOnMissingBad = false) { let rval = null; if (!!event && !!event.headers) { const headerVal = EventUtil.extractAuthorizationHeaderCaseInsensitive(event); if (!!headerVal && headerVal.startsWith('Basic ')) { const parsed = Base64Ratchet.base64StringToString(headerVal.substring(6)); const sp = parsed.split(':'); Logger.silly('Parsed to %j', sp); if (!!sp && sp.length === 2) { rval = { username: sp[0], password: sp[1], }; } } } if (!rval && throwErrorOnMissingBad) { throw new UnauthorizedError('Could not find valid basic authentication header'); } return rval; } static eventIsAGraphQLIntrospection(event) { let rval = false; if (event) { if (!!event.httpMethod && 'post' === event.httpMethod.toLowerCase()) { if (!!event.path && event.path.endsWith('/graphql')) { try { const body = EventUtil.jsonBodyToObject(event); rval = !!body && !!body['operationName'] && body['operationName'] === 'IntrospectionQuery'; } catch (err) { Logger.error('Failed to parse body - treating as non-graphql : %s : %s', event?.body, err, err); rval = false; } } } } return rval; } static extractAuthorizationHeaderCaseInsensitive(evt) { return MapRatchet.caseInsensitiveAccess(evt?.headers || {}, EpsilonConstants.AUTH_HEADER_NAME); } static extractBearerTokenFromEvent(evt) { let rval = null; const authHeader = StringRatchet.trimToEmpty(EventUtil.extractAuthorizationHeaderCaseInsensitive(evt)); if (authHeader.toLowerCase().startsWith('bearer ')) { rval = authHeader.substring(7); } return rval; } static hostMatchesRegexInList(host, list, caseSensitive) { let rval = false; if (StringRatchet.trimToNull(host) && list?.length) { const test = StringRatchet.trimToEmpty(caseSensitive ? host : host.toLowerCase()); rval = !!list.find((l) => test.match(l)); } else { Logger.warn('Not matching regex - either host or list is misconfigured : %s : %j', host, list); } return rval; } static hostIsLocal(host) { return EventUtil.hostMatchesRegexInList(host, EventUtil.LOCAL_REGEX); } static hostIsLocalOrNotRoutableIP4(host) { return EventUtil.hostMatchesRegexInList(host, EventUtil.NON_ROUTABLE_REGEX); } } //# sourceMappingURL=event-util.js.map