@webda/aws
Version:
Webda AWS Services implementation
264 lines • 10.1 kB
JavaScript
import { Core as Webda, HttpContext, WebdaError } from "@webda/core";
import { serialize as cookieSerialize } from "cookie";
/**
* The Lambda entrypoint for Webda
*
* This take the input coming from the API Gateway to transform it and analyse it with Webda
* Once execution is done, it will format the result in a way that the API Gateway will output the result
* You need to use the Webda deployment so the API Gateway has all the right templates in place
*
* @class
*/
export default class LambdaServer extends Webda {
constructor() {
super(...arguments);
this._awsEventsHandlers = [];
}
/**
* @ignore
*/
flushHeaders(ctx) {
const headers = ctx.getResponseHeaders() || {};
this._result = {
headers,
statusCode: ctx.statusCode
};
let cookies = ctx.getResponseCookies();
this._result.multiValueHeaders = { "Set-Cookie": [] };
for (let i in cookies) {
this._result.multiValueHeaders["Set-Cookie"].push(cookieSerialize(cookies[i].name, cookies[i].value, cookies[i].options || {}));
}
}
/**
* @inheritdoc
*/
flush(ctx) {
const body = ctx.getResponseBody();
if (body !== undefined && body !== false) {
this._result.body = body;
}
}
/**
* Register a service to handle AWS Events
* @param service
*/
registerAWSEventsHandler(service) {
if (this._awsEventsHandlers.indexOf(service) < 0) {
this._awsEventsHandlers.push(service);
}
}
async handleAWSEvent(source, events) {
for (let i in this._awsEventsHandlers) {
let handler = this._awsEventsHandlers[i];
if (handler.isAWSEventHandled(source, events)) {
await handler.handleAWSEvent(source, events);
}
}
}
/**
* Analyse events to try to find its type
* @param events
* @returns
*/
async handleAWSEvents(events) {
if (events.Records) {
await this.handleAWSEvent(events.Records[0].eventSource, events);
return true;
}
else if (events.invocationId && events.records) {
await this.handleAWSEvent("aws:kinesis", events);
return true;
}
else if (events["detail-type"] && events.detail && events.resources) {
await this.handleAWSEvent("aws:scheduled-event", events);
return true;
}
else if (events.awslogs) {
await this.handleAWSEvent("aws:cloudwatch-logs", events);
return true;
}
else if (events["CodePipeline.job"]) {
await this.handleAWSEvent("aws:codepipeline", events);
return true;
}
else if (events.identityPoolId) {
await this.handleAWSEvent("aws:cognito", events);
return true;
}
else if (events.configRuleId) {
await this.handleAWSEvent("aws:config", events);
return true;
}
else if (events.jobDefinition || events.jobId) {
await this.handleAWSEvent("aws:batch", events);
return true;
}
return false;
}
/**
* Need to unit test this part, with sample of data coming from the API Gateway
*
* @ignore
*/
async handleRequest(sourceEvent, context) {
await this.init();
// Handle AWS event
if (await this.handleAWSEvents(sourceEvent)) {
this.log("INFO", "Handled AWS event", sourceEvent);
return;
}
// Manual launch of webda
if (sourceEvent.command === "launch" && sourceEvent.service && sourceEvent.method) {
let commandEvent = sourceEvent;
let args = commandEvent.args || [];
this.log("INFO", "Executing", commandEvent.method, "on", commandEvent.service, "with", args);
let service = this.getService(commandEvent.service);
if (!service) {
this.log("ERROR", "Cannot find", commandEvent.service);
return;
}
if (typeof service[commandEvent.method] !== "function") {
this.log("ERROR", "Cannot find method", commandEvent.method, "on", commandEvent.service);
return;
}
await service[commandEvent.method](...args);
this.log("INFO", "Finished");
return;
}
let event = sourceEvent;
context.callbackWaitsForEmptyEventLoop =
(this.getConfiguration().parameters && this.getConfiguration().parameters.waitForEmptyEventLoop) || false;
this._result = {};
let vhost;
let i;
let headers = event.headers || {};
vhost = headers.Host;
let method = event.httpMethod || "GET";
let protocol = headers["CloudFront-Forwarded-Proto"] || "https";
let port = headers["X-Forwarded-Port"] || 443;
if (typeof port === "string") {
port = Number(port);
if (isNaN(port)) {
port = 443;
}
}
let resourcePath = event.path;
// Rebuild query string
if (event.queryStringParameters) {
let sep = "?";
for (i in event.queryStringParameters) {
// If additional error code it will be contained so need to check for &
// May need to add urlencode
resourcePath += sep + i + "=" + event.queryStringParameters[i];
sep = "&";
}
}
this.log("INFO", event.httpMethod || "GET", event.path);
let httpContext = new HttpContext(vhost, method, resourcePath, protocol, port, headers).setClientIp(headers["X-Real-Ip"]); // Might use identity.sourceIp
if (["PUT", "PATCH", "POST", "DELETE"].includes(method)) {
httpContext.setBody(event.body);
}
this.computePrefix(event, httpContext);
let ctx = await this.newWebContext(httpContext);
// TODO Get all client info
// event['requestContext']['identity']['sourceIp']
// Debug mode
await this.emitSync("Webda.Request", { context: ctx });
if (this.getConfiguration().parameters.lambdaRequestHeader) {
ctx.setHeader(this.getConfiguration().parameters.lambdaRequestHeader, context.awsRequestId);
}
let origin = headers.Origin || headers.origin;
try {
// Set predefined headers for CORS
if (!origin || (await this.checkCORSRequest(ctx))) {
if (origin) {
ctx.setHeader("Access-Control-Allow-Origin", origin);
}
}
else {
// Prevent CSRF
this.log("INFO", "CSRF denied from", origin);
ctx.statusCode = 401;
return this.handleLambdaReturn(ctx);
}
// Check request overall
if (!(await this.checkRequest(ctx))) {
this.log("WARN", "Request refused");
throw 403;
}
if (protocol === "https") {
// Add the HSTS header
ctx.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
}
// Might want to customize this one
ctx.setHeader("Access-Control-Max-Age", 3600);
ctx.setHeader("Access-Control-Allow-Credentials", "true");
ctx.setHeader("Access-Control-Allow-Headers", headers["access-control-request-headers"] || "content-type");
if (method === "OPTIONS") {
// Return allow all methods for now
let routes = this.router.getRouteMethodsFromUrl(ctx.getHttpContext().getRelativeUri());
if (routes.length == 0) {
ctx.statusCode = 404;
return this.handleLambdaReturn(ctx);
}
routes.push("OPTIONS");
ctx.setHeader("Access-Control-Allow-Methods", routes.join(","));
return this.handleLambdaReturn(ctx);
}
if (!this.updateContextWithRoute(ctx)) {
this.emitSync("Webda.404", { context: ctx });
ctx.statusCode = 404;
return this.handleLambdaReturn(ctx);
}
await ctx.init();
await ctx.execute();
return this.handleLambdaReturn(ctx);
}
catch (err) {
if (typeof err === "number") {
ctx.statusCode = err;
}
else if (err instanceof WebdaError.HttpError) {
this.log("DEBUG", "Sending error", err.message);
ctx.statusCode = err.getResponseCode();
// Handle redirect
if (err instanceof WebdaError.Redirect) {
ctx.setHeader("Location", err.location);
}
}
else {
this.log("ERROR", err);
ctx.statusCode = 500;
}
return this.handleLambdaReturn(ctx);
}
}
/**
* Based on API Gateway event compute the prefix if any
* @param event
* @param httpContext
*/
computePrefix(event, httpContext) {
if (event.path !== event.resource) {
let relativeUri = event.resource;
for (let j in event.pathParameters) {
relativeUri = relativeUri.replace(new RegExp(`\\{${j}\\+?\\}`), event.pathParameters[j]);
}
if (relativeUri !== event.path) {
httpContext.setPrefix(event.path.substr(0, event.path.length - relativeUri.length));
}
}
}
/**
* Collect result from Context to this._result and return it
* @param context
* @returns
*/
async handleLambdaReturn(context) {
await this.emitSync("Webda.Result", { context });
await context.end();
return this._result;
}
}
export { LambdaServer };
//# sourceMappingURL=lambdaserver.js.map