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