UNPKG

@bitblit/ratchet-epsilon-common

Version:

Tiny adapter to simplify building API gateway Lambda APIS

358 lines 16.7 kB
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&amp;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