lambda-api
Version:
Lightweight web framework for your serverless applications
360 lines (301 loc) • 10.8 kB
JavaScript
'use strict';
/**
* Lightweight web framework for your serverless applications
* @author Jeremy Daly <jeremy@jeremydaly.com>
* @license MIT
*/
const QS = require('querystring'); // Require the querystring library
const UTILS = require('./utils'); // Require utils library
const LOGGER = require('./logger'); // Require logger library
const { RouteError, MethodError } = require('./errors'); // Require custom errors
class REQUEST {
// Create the constructor function.
constructor(app) {
// Record start time
this._start = Date.now();
// Create a reference to the app
this.app = app;
// Flag cold starts
this.coldStart = app._requestCount === 0 ? true : false;
// Increment the requests counter
this.requestCount = ++app._requestCount;
// Init the handler
this._handler;
// Init the execution stack
this._stack;
// Expose Namespaces
this.namespace = this.ns = app._app;
// Set the version
this.version = app._version;
// Init the params
this.params = {};
// Init headers
this.headers = {};
// Init multi-value support flag
this._multiValueSupport = null;
// Init log helpers (message,custom) and create app reference
app.log = this.log = Object.keys(app._logLevels).reduce(
(acc, lvl) =>
Object.assign(acc, {
[lvl]: (m, c) => this.logger(lvl, m, this, this.context, c),
}),
{}
);
// Init _logs array for storage
this._logs = [];
} // end constructor
// Parse the request
async parseRequest() {
// Set the payload version
this.payloadVersion = this.app._event.version
? this.app._event.version
: null;
// Detect multi-value support
this._multiValueSupport = 'multiValueHeaders' in this.app._event;
// Set the method
this.method = this.app._event.httpMethod
? this.app._event.httpMethod.toUpperCase()
: this.app._event.requestContext && this.app._event.requestContext.http
? this.app._event.requestContext.http.method.toUpperCase()
: 'GET';
// Set the path
this.path =
this.payloadVersion === '2.0'
? this.app._event.rawPath
: this.app._event.path;
// Set the query parameters (backfill for ALB)
this.query = Object.assign(
{},
this.app._event.queryStringParameters,
'queryStringParameters' in this.app._event
? {} // do nothing
: Object.keys(
Object.assign({}, this.app._event.multiValueQueryStringParameters)
).reduce(
(qs, key) =>
Object.assign(
qs, // get the last value of the array
{
[key]: decodeURIComponent(
this.app._event.multiValueQueryStringParameters[key].slice(
-1
)[0]
),
}
),
{}
)
);
// Set the multi-value query parameters (simulate if no multi-value support)
this.multiValueQuery = Object.assign(
{},
this._multiValueSupport
? {}
: Object.keys(this.query).reduce(
(qs, key) =>
Object.assign(qs, { [key]: this.query[key].split(',') }),
{}
),
this.app._event.multiValueQueryStringParameters
);
// Set the raw headers (normalize multi-values)
// per https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
this.rawHeaders =
this._multiValueSupport && this.app._event.multiValueHeaders !== null
? Object.keys(this.app._event.multiValueHeaders).reduce(
(headers, key) =>
Object.assign(headers, {
[key]: UTILS.fromArray(this.app._event.multiValueHeaders[key]),
}),
{}
)
: this.app._event.headers || {};
// Set the headers to lowercase
this.headers = Object.keys(this.rawHeaders).reduce(
(acc, header) =>
Object.assign(acc, { [header.toLowerCase()]: this.rawHeaders[header] }),
{}
);
this.multiValueHeaders = this._multiValueSupport
? this.app._event.multiValueHeaders
: Object.keys(this.headers).reduce(
(headers, key) =>
Object.assign(headers, {
[key.toLowerCase()]: this.headers[key].split(','),
}),
{}
);
// Extract user agent
this.userAgent = this.headers['user-agent'];
// Get cookies from event
let cookies = this.app._event.cookies
? this.app._event.cookies
: this.headers.cookie
? this.headers.cookie.split(';')
: [];
// Set and parse cookies
this.cookies = cookies.reduce((acc, cookie) => {
cookie = cookie.trim().split('=');
return Object.assign(acc, {
[cookie[0]]: UTILS.parseBody(
decodeURIComponent(cookie.slice(1).join('='))
),
});
}, {});
// Attempt to parse the auth
this.auth = UTILS.parseAuth(this.headers.authorization);
// Set the requestContext
this.requestContext = this.app._event.requestContext || {};
// Extract IP (w/ sourceIp fallback)
this.ip =
(this.headers['x-forwarded-for'] &&
this.headers['x-forwarded-for'].split(',')[0].trim()) ||
(this.requestContext['identity'] &&
this.requestContext['identity']['sourceIp'] &&
this.requestContext['identity']['sourceIp'].split(',')[0].trim());
// Assign the requesting interface
this.interface = this.requestContext.elb ? 'alb' : 'apigateway';
// Set the pathParameters
this.pathParameters = this.app._event.pathParameters || {};
// Set the stageVariables
this.stageVariables = this.app._event.stageVariables || {};
// Set the isBase64Encoded
this.isBase64Encoded = this.app._event.isBase64Encoded || false;
// Add context
this.context =
this.app.context && typeof this.app.context === 'object'
? this.app.context
: {};
// Parse id from context
this.id = this.context.awsRequestId ? this.context.awsRequestId : null;
// Determine client type
this.clientType =
this.headers['cloudfront-is-desktop-viewer'] === 'true'
? 'desktop'
: this.headers['cloudfront-is-mobile-viewer'] === 'true'
? 'mobile'
: this.headers['cloudfront-is-smarttv-viewer'] === 'true'
? 'tv'
: this.headers['cloudfront-is-tablet-viewer'] === 'true'
? 'tablet'
: 'unknown';
// Parse country
this.clientCountry = this.headers['cloudfront-viewer-country']
? this.headers['cloudfront-viewer-country'].toUpperCase()
: 'unknown';
// Capture the raw body
this.rawBody = this.app._event.body;
// Set the body (decode it if base64 encoded)
this.body = this.app._event.isBase64Encoded
? Buffer.from(this.app._event.body || '', 'base64').toString()
: this.app._event.body;
// Set the body
if (
this.headers['content-type'] &&
this.headers['content-type'].includes('application/x-www-form-urlencoded')
) {
this.body = QS.parse(this.body);
} else if (typeof this.body === 'object') {
// Do nothing
} else {
this.body = UTILS.parseBody(this.body);
}
// Init the stack reporter
this.stack = null;
// Extract path from event (strip querystring just in case)
let path = UTILS.parsePath(this.path);
// Init the route
this.route = null;
// Create a local routes reference
let routes = this.app._routes;
// Init wildcard
let wc = [];
// Loop the routes and see if this matches
for (let i = 0; i < path.length; i++) {
// Capture wildcard routes
if (routes['ROUTES'] && routes['ROUTES']['*']) {
wc.push(routes['ROUTES']['*']);
}
// Traverse routes
if (routes['ROUTES'] && routes['ROUTES'][path[i]]) {
routes = routes['ROUTES'][path[i]];
} else if (routes['ROUTES'] && routes['ROUTES']['__VAR__']) {
routes = routes['ROUTES']['__VAR__'];
} else if (
wc[wc.length - 1] &&
wc[wc.length - 1]['METHODS'] &&
// && (wc[wc.length-1]['METHODS'][this.method] || wc[wc.length-1]['METHODS']['ANY'])
((this.method !== 'OPTIONS' &&
Object.keys(wc[wc.length - 1]['METHODS']).toString() !== 'OPTIONS') ||
this.validWildcard(wc, this.method))
) {
routes = wc[wc.length - 1];
} else {
this.app._errorStatus = 404;
throw new RouteError('Route not found', '/' + path.join('/'));
}
} // end for loop
// Grab the deepest wildcard path
let wildcard = wc.pop();
// Select ROUTE if exist for method, default ANY, apply wildcards, alias HEAD requests
let route =
routes['METHODS'] && routes['METHODS'][this.method]
? routes['METHODS'][this.method]
: routes['METHODS'] && routes['METHODS']['ANY']
? routes['METHODS']['ANY']
: wildcard && wildcard['METHODS'] && wildcard['METHODS'][this.method]
? wildcard['METHODS'][this.method]
: wildcard && wildcard['METHODS'] && wildcard['METHODS']['ANY']
? wildcard['METHODS']['ANY']
: this.method === 'HEAD' &&
routes['METHODS'] &&
routes['METHODS']['GET']
? routes['METHODS']['GET']
: undefined;
// Check for the requested method
if (route) {
// Assign path parameters
for (let x in route.vars) {
route.vars[x].map((y) => (this.params[y] = path[x]));
} // end for
// Set the route used
this.route = route.route;
// Set the execution stack
// this._stack = route.inherited.concat(route.stack);
this._stack = route.stack;
// Set the stack reporter
this.stack = this._stack.map((x) =>
x.name.trim() !== '' ? x.name : 'unnamed'
);
} else {
this.app._errorStatus = 405;
throw new MethodError(
'Method not allowed',
this.method,
'/' + path.join('/')
);
}
// Reference to sample rule
this._sampleRule = {};
// Enable sampling
this._sample = LOGGER.sampler(this.app, this);
} // end parseRequest
// Main logger
logger(...args) {
this.app._logger.level !== 'none' &&
this.app._logLevels[args[0]] >=
this.app._logLevels[
this._sample ? this._sample : this.app._logger.level
] &&
this._logs.push(this.app._logger.log(...args));
}
// Recursive wildcard function
validWildcard(wc) {
return (
Object.keys(wc[wc.length - 1]['METHODS']).length > 1 ||
(wc.length > 1 && this.validWildcard(wc.slice(0, -1)))
);
}
} // end REQUEST class
// Export the response object
module.exports = REQUEST;