@bitblit/ratchet-epsilon-common
Version:
Tiny adapter to simplify building API gateway Lambda APIS
358 lines • 16.7 kB
JavaScript
import { Logger } from '@bitblit/ratchet-common/logger/logger';
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 http from 'http';
import https from 'https';
import { DateTime } from 'luxon';
import { EventUtil } from './http/event-util.js';
import { SampleServerComponents } from './sample/sample-server-components.js';
import { LocalWebTokenManipulator } from './http/auth/local-web-token-manipulator.js';
import { LocalServerHttpMethodHandling } from './config/local-server/local-server-http-method-handling.js';
import { LocalServerCert } from '@bitblit/ratchet-node-only/http/local-server-cert';
import { EpsilonConstants } from './epsilon-constants.js';
import { AbstractBackgroundManager } from './background/manager/abstract-background-manager.js';
import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
import { ResponseUtil } from "./http/response-util.js";
import { NumberRatchet } from "@bitblit/ratchet-common/lang/number-ratchet";
import { LocalServerEventLoggingStyle } from "./config/local-server/local-server-event-logging-style.js";
export class LocalServer {
globalHandler;
server;
options;
constructor(globalHandler, inOpts) {
this.globalHandler = globalHandler;
this.options = {
port: inOpts?.port ?? 8888,
https: inOpts?.https ?? false,
methodHandling: inOpts?.methodHandling ?? LocalServerHttpMethodHandling.Lowercase,
eventLoggingLevel: inOpts?.eventLoggingLevel ?? LoggerLevelName.debug,
eventLoggingStyle: inOpts?.eventLoggingStyle ?? LocalServerEventLoggingStyle.Summary,
graphQLIntrospectionEventLogLevel: inOpts?.graphQLIntrospectionEventLogLevel ?? LoggerLevelName.silly
};
}
async runServer() {
return new Promise((res, rej) => {
try {
Logger.info('Starting Epsilon server on port %d', this.options.port);
this.server = LocalServer.createNodeServer(this.options, this.requestHandler.bind(this));
Logger.info('Epsilon server is listening');
process.on('SIGINT', () => {
Logger.info('Caught SIGINT - shutting down test server...');
this.server.close();
res(true);
});
}
catch (err) {
Logger.error('Local server failed : %s', err, err);
rej(err);
}
});
}
static createNodeServer(opts, requestHandler) {
let rval = null;
if (opts.https) {
const options = {
key: LocalServerCert.CLIENT_KEY_PEM,
cert: LocalServerCert.CLIENT_CERT_PEM,
};
Logger.info('Starting https server - THIS SERVER IS NOT SECURE! The KEYS are in the code! Testing Server Only - Use at your own risk!');
rval = https.createServer(options, requestHandler).listen(opts.port);
}
else {
rval = http.createServer(requestHandler).listen(opts.port);
}
return rval;
}
async requestHandler(request, response) {
const context = {
awsRequestId: 'LOCAL-' + StringRatchet.createType4Guid(),
getRemainingTimeInMillis() {
return 300000;
},
};
const evt = await LocalServer.messageToApiGatewayEvent(request, context, this.options);
if (evt.path.startsWith('/epsilon-poison-pill')) {
this.server.close(() => {
Logger.info('Server closed');
});
return true;
}
else if (evt.path.startsWith('/epsilon-background-launcher')) {
Logger.info('Showing background launcher page');
const names = this.globalHandler.epsilon.backgroundHandler.validProcessorNames;
response.end(LocalServer.buildBackgroundTriggerFormHtml(names));
return true;
}
else if (evt.path.startsWith('/epsilon-background-trigger')) {
Logger.info('Running background trigger');
try {
const entry = LocalServer.parseEpsilonBackgroundTriggerAsTask(evt);
const processed = await this.globalHandler.processSingleBackgroundEntry(entry);
response.end(`<html><body>BG TRIGGER VALID, returned ${processed} : task : ${entry.type} : data: ${entry.data}</body></html>`);
}
catch (err) {
response.end(`<html><body>BG TRIGGER FAILED : Error : ${err}</body></html>`);
}
return true;
}
else {
const result = await this.globalHandler.lambdaHandler(evt, context);
const written = await LocalServer.writeProxyResultToServerResponse(result, response, evt, this.options);
return written;
}
}
static parseEpsilonBackgroundTriggerAsTask(evt) {
Logger.info('Running background trigger');
const taskName = StringRatchet.trimToNull(evt.queryStringParameters['task']);
let dataJson = StringRatchet.trimToNull(evt.queryStringParameters['dataJson']);
let metaJson = StringRatchet.trimToNull(evt.queryStringParameters['metaJson']);
dataJson = dataJson ? ResponseUtil.decodeUriComponentAndReplacePlus(dataJson) : dataJson;
metaJson = metaJson ? ResponseUtil.decodeUriComponentAndReplacePlus(metaJson) : metaJson;
let error = '';
error += taskName ? '' : 'No task provided';
let data = null;
let _meta = null;
try {
if (dataJson) {
data = JSON.parse(dataJson);
}
}
catch (err) {
error += 'Data is not valid JSON : ' + err + ' WAS: ' + dataJson;
}
try {
if (metaJson) {
_meta = JSON.parse(metaJson);
}
}
catch (err) {
error += 'Meta is not valid JSON : ' + err + ' WAS: ' + metaJson;
}
if (error.length > 0) {
throw ErrorRatchet.throwFormattedErr('Errors %j', error);
}
const rval = {
type: taskName,
data: data,
};
return rval;
}
static async bodyAsBase64String(request) {
return new Promise((res, _rej) => {
const body = [];
request.on('data', (chunk) => {
body.push(chunk);
});
request.on('end', () => {
const rval = Buffer.concat(body).toString('base64');
res(rval);
});
});
}
static async messageToApiGatewayEvent(request, context, options) {
const bodyString = await LocalServer.bodyAsBase64String(request);
const stageIdx = request.url.indexOf('/', 1);
const stage = request.url.substring(1, stageIdx);
const path = request.url.substring(stageIdx + 1);
const reqTime = new Date().getTime();
const formattedTime = DateTime.utc().toFormat('dd/MMM/yyyy:hh:mm:ss ZZ');
const queryStringParams = LocalServer.parseQueryParamsFromUrlString(path);
const headers = Object.assign({}, request.headers);
headers['X-Forwarded-Proto'] = 'http';
let targetMethod = StringRatchet.trimToEmpty(request.method);
targetMethod = options?.methodHandling === LocalServerHttpMethodHandling.Lowercase ? targetMethod.toLowerCase() : targetMethod;
targetMethod = options?.methodHandling === LocalServerHttpMethodHandling.Uppercase ? targetMethod.toUpperCase() : targetMethod;
const rval = {
body: bodyString,
multiValueHeaders: {},
multiValueQueryStringParameters: {},
resource: '/{proxy+}',
path: request.url,
httpMethod: targetMethod,
isBase64Encoded: true,
queryStringParameters: queryStringParams,
pathParameters: {
proxy: path,
},
stageVariables: {
baz: 'qux',
},
headers: headers,
requestContext: {
accountId: '123456789012',
resourceId: '123456',
stage: stage,
requestId: context.awsRequestId,
requestTime: formattedTime,
requestTimeEpoch: reqTime,
identity: {
apiKeyId: null,
clientCert: null,
principalOrgId: null,
apiKey: null,
cognitoIdentityPoolId: null,
accountId: null,
cognitoIdentityId: null,
caller: null,
accessKey: null,
sourceIp: '127.0.0.1',
cognitoAuthenticationType: null,
cognitoAuthenticationProvider: null,
userArn: null,
userAgent: 'Custom User Agent String',
user: null,
},
path: request.url,
domainName: request.headers['host'],
resourcePath: '/{proxy+}',
httpMethod: request.method.toLowerCase(),
apiId: '1234567890',
protocol: 'HTTP/1.1',
authorizer: null,
},
};
return rval;
}
static createBackgroundSNSEvent(entry) {
const internal = Object.assign({}, entry, {
createdEpochMS: new Date().getTime(),
guid: AbstractBackgroundManager.generateBackgroundGuid(),
traceId: 'FAKE-TRACE-' + StringRatchet.createType4Guid(),
traceDepth: 1,
});
const toWrite = {
type: EpsilonConstants.BACKGROUND_SNS_IMMEDIATE_RUN_FLAG,
backgroundEntry: internal,
};
const rval = {
Records: [
{
EventVersion: '1.0',
EventSubscriptionArn: 'arn:aws:sns:us-east-1:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486',
EventSource: 'aws:sns',
Sns: {
SignatureVersion: '1',
Timestamp: '2019-01-02T12:45:07.000Z',
Signature: 'tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==',
SigningCertUrl: 'https://sns.us-east-1.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem',
MessageId: '95df01b4-ee98-5cb9-9903-4c221d41eb5e',
Message: JSON.stringify(toWrite),
MessageAttributes: {},
Type: 'Notification',
UnsubscribeUrl: 'https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486',
TopicArn: 'arn:aws:sns:us-east-1:123456789012:sns-lambda',
Subject: 'EpsilonBackgroundInvoke',
},
},
],
};
return rval;
}
static isProxyResult(val) {
return val && NumberRatchet.safeNumber(val.statusCode) !== null && StringRatchet.trimToNull(val.body) !== null;
}
static summarizeResponse(proxyResult, sourceEvent) {
const summary = {
srcPath: sourceEvent.path,
resultCode: proxyResult.statusCode,
resultIsBase64: proxyResult.isBase64Encoded,
resultBytes: (proxyResult.body ?? '').length
};
return summary;
}
static async writeProxyResultToServerResponse(proxyResult, response, sourceEvent, options) {
const logEventLevel = EventUtil.eventIsAGraphQLIntrospection(sourceEvent) ? options.graphQLIntrospectionEventLogLevel : options.eventLoggingLevel;
switch (options.eventLoggingStyle) {
case 'Full':
Logger.logByLevel(logEventLevel, 'Result: %j', proxyResult);
break;
case 'FullWithBase64Decode':
if (proxyResult.isBase64Encoded) {
const dup = structuredClone(proxyResult);
dup.body = Base64Ratchet.base64StringToString(dup.body);
dup.isBase64Encoded = false;
Logger.logByLevel(logEventLevel, 'Result (UB64): %j', dup);
}
else {
Logger.logByLevel(logEventLevel, 'Result: %j', proxyResult);
}
break;
case 'Summary':
Logger.logByLevel(logEventLevel, 'Result (summary): %j', LocalServer.summarizeResponse(proxyResult, sourceEvent));
break;
case 'None':
break;
default:
throw new Error('Should not happen - full enumeration');
}
response.statusCode = proxyResult.statusCode ?? 500;
if (proxyResult.headers) {
Object.keys(proxyResult.headers).forEach((hk) => {
response.setHeader(hk, String(proxyResult.headers[hk]));
});
}
if (proxyResult.multiValueHeaders) {
Object.keys(proxyResult.multiValueHeaders).forEach((hk) => {
response.setHeader(hk, proxyResult.multiValueHeaders[hk].join(','));
});
}
const toWrite = proxyResult.isBase64Encoded ? Buffer.from(proxyResult.body, 'base64') : Buffer.from(proxyResult.body);
response.end(toWrite);
return !!proxyResult.body;
}
static parseQueryParamsFromUrlString(urlString) {
const rval = {};
const searchStringParts = urlString.split('?');
if (searchStringParts.length < 2) {
return rval;
}
const searchString = searchStringParts.slice(1).join('?');
const searchParts = searchString.split('&');
for (const eachKeyValueString of searchParts) {
const eachKeyValueStringParts = eachKeyValueString.split('=');
const eachKey = eachKeyValueStringParts[0];
const eachValue = eachKeyValueStringParts.slice(1).join('=');
rval[eachKey] = eachValue;
}
return rval;
}
static async runSampleBatchOnlyServerFromCliArgs(_args) {
Logger.setLevel(LoggerLevelName.debug);
const handler = await SampleServerComponents.createSampleBatchOnlyEpsilonGlobalHandler('SampleBatchOnlyLocalServer-' + Date.now());
const testServer = new LocalServer(handler);
const res = await testServer.runServer();
Logger.info('Res was : %s', res);
}
static async runSampleLocalServerFromCliArgs(_args) {
Logger.setLevel(LoggerLevelName.debug);
const localTokenHandler = new LocalWebTokenManipulator(['abcd1234'], 'sample-server');
const token = await localTokenHandler.createJWTStringAsync('asdf', {}, ['USER'], 3600);
Logger.info('Use token: %s', token);
const handler = await SampleServerComponents.createSampleEpsilonGlobalHandler('SampleLocalServer-' + Date.now());
const testServer = new LocalServer(handler, { port: 8888, https: true });
const res = await testServer.runServer();
Logger.info('Res was : %s', res);
}
static buildBackgroundTriggerFormHtml(names) {
let html = '<html><head><title>Epsilon BG Launcher</title></head><body><div>';
html += '<h1>Epsilon Background Launcher</h1><form method="GET" action="/epsilon-background-trigger">';
html += '<div style="display: flex; flex-direction: column">';
if (names) {
html += '<label for="task">Task Name</label><select id="task" name="task">';
names.forEach((n) => {
html += `<option value="${n}">${n}</option>`;
});
html += '</select>';
}
else {
html += '<label for="task">Task Name</label><input type="text" id="task" name="task"></input>';
}
html += '<label for="dataJson">Data JSON</label><textarea id="dataJson" name="dataJson">{}</textarea>';
html += '<label for="metaJson">Meta JSON</label><textarea id="metaJson" name="metaJson">{}</textarea>';
html += '<input type="submit" value="Submit">';
html += '</div></form></div></body></html>';
return html;
}
}
//# sourceMappingURL=local-server.js.map