/node_modules/serverless-offline/src/index.js

https://bitbucket.org/lambdalamas/always · JavaScript · 1009 lines · 741 code · 180 blank · 88 comment · 144 complexity · 9227a430ed6b3d6dbcf13ad7e02cdca4 MD5 · raw file

  1. 'use strict';
  2. // Node dependencies
  3. const fs = require('fs');
  4. const path = require('path');
  5. const exec = require('child_process').exec;
  6. // External dependencies
  7. const Hapi = require('hapi');
  8. const corsHeaders = require('hapi-cors-headers');
  9. const _ = require('lodash');
  10. const crypto = require('crypto');
  11. // Internal lib
  12. require('./javaHelper');
  13. const debugLog = require('./debugLog');
  14. const jsonPath = require('./jsonPath');
  15. const createLambdaContext = require('./createLambdaContext');
  16. const createVelocityContext = require('./createVelocityContext');
  17. const createLambdaProxyContext = require('./createLambdaProxyContext');
  18. const renderVelocityTemplateObject = require('./renderVelocityTemplateObject');
  19. const createAuthScheme = require('./createAuthScheme');
  20. const functionHelper = require('./functionHelper');
  21. const Endpoint = require('./Endpoint');
  22. const parseResources = require('./parseResources');
  23. /*
  24. I'm against monolithic code like this file, but splitting it induces unneeded complexity.
  25. */
  26. class Offline {
  27. constructor(serverless, options) {
  28. this.serverless = serverless;
  29. this.service = serverless.service;
  30. this.serverlessLog = serverless.cli.log.bind(serverless.cli);
  31. this.options = options;
  32. this.exitCode = 0;
  33. this.provider = 'aws';
  34. this.start = this.start.bind(this);
  35. this.commands = {
  36. offline: {
  37. usage: 'Simulates API Gateway to call your lambda functions offline.',
  38. lifecycleEvents: ['start'],
  39. // add start nested options
  40. commands: {
  41. start: {
  42. usage: 'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
  43. lifecycleEvents: [
  44. 'init',
  45. 'end',
  46. ],
  47. },
  48. },
  49. options: {
  50. prefix: {
  51. usage: 'Adds a prefix to every path, to send your requests to http://localhost:3000/prefix/[your_path] instead.',
  52. shortcut: 'p',
  53. },
  54. host: {
  55. usage: 'The host name to listen on. Default: localhost',
  56. shortcut: 'o',
  57. },
  58. port: {
  59. usage: 'Port to listen on. Default: 3000',
  60. shortcut: 'P',
  61. },
  62. stage: {
  63. usage: 'The stage used to populate your templates.',
  64. shortcut: 's',
  65. },
  66. region: {
  67. usage: 'The region used to populate your templates.',
  68. shortcut: 'r',
  69. },
  70. skipCacheInvalidation: {
  71. usage: 'Tells the plugin to skip require cache invalidation. A script reloading tool like Nodemon might then be needed',
  72. shortcut: 'c',
  73. },
  74. httpsProtocol: {
  75. usage: 'To enable HTTPS, specify directory (relative to your cwd, typically your project dir) for both cert.pem and key.pem files.',
  76. shortcut: 'H',
  77. },
  78. location: {
  79. usage: 'The root location of the handlers\' files.',
  80. shortcut: 'l',
  81. },
  82. noTimeout: {
  83. usage: 'Disable the timeout feature.',
  84. shortcut: 't',
  85. },
  86. noEnvironment: {
  87. usage: 'Turns off loading of your environment variables from serverless.yml. Allows the usage of tools such as PM2 or docker-compose.',
  88. },
  89. resourceRoutes: {
  90. usage: 'Turns on loading of your HTTP proxy settings from serverless.yml.',
  91. },
  92. dontPrintOutput: {
  93. usage: 'Turns off logging of your lambda outputs in the terminal.',
  94. },
  95. corsAllowOrigin: {
  96. usage: 'Used to build the Access-Control-Allow-Origin header for CORS support.',
  97. },
  98. corsAllowHeaders: {
  99. usage: 'Used to build the Access-Control-Allow-Headers header for CORS support.',
  100. },
  101. corsDisallowCredentials: {
  102. usage: 'Used to override the Access-Control-Allow-Credentials default (which is true) to false.',
  103. },
  104. apiKey: {
  105. usage: 'Defines the api key value to be used for endpoints marked as private. Defaults to a random hash.',
  106. },
  107. exec: {
  108. usage: 'When provided, a shell script is executed when the server starts up, and the server will shut down after handling this command.',
  109. },
  110. noAuth: {
  111. usage: 'Turns off all authorizers',
  112. },
  113. },
  114. },
  115. };
  116. this.hooks = {
  117. 'offline:start:init': this.start.bind(this),
  118. 'offline:start': this.start.bind(this),
  119. 'offline:start:end': this.end.bind(this),
  120. };
  121. }
  122. printBlankLine() {
  123. console.log();
  124. }
  125. logPluginIssue() {
  126. this.serverlessLog('If you think this is an issue with the plugin please submit it, thanks!');
  127. this.serverlessLog('https://github.com/dherault/serverless-offline/issues');
  128. }
  129. // Entry point for the plugin (sls offline)
  130. start() {
  131. this._checkVersion();
  132. // Some users would like to know their environment outside of the handler
  133. process.env.IS_OFFLINE = true;
  134. return Promise.resolve(this._buildServer())
  135. .then(() => this._listen())
  136. .then(() => this.options.exec ? this._executeShellScript() : this._listenForSigInt())
  137. .then(() => this.end());
  138. }
  139. _checkVersion() {
  140. const version = this.serverless.version;
  141. if (!version.startsWith('1.')) {
  142. this.serverlessLog(`Offline requires Serverless v1.x.x but found ${version}. Exiting.`);
  143. process.exit(0);
  144. }
  145. }
  146. _listenForSigInt() {
  147. // Listen for ctrl+c to stop the server
  148. return new Promise(resolve => {
  149. process.on('SIGINT', () => {
  150. this.serverlessLog('Offline Halting...');
  151. resolve();
  152. });
  153. });
  154. }
  155. _executeShellScript() {
  156. const command = this.options.exec;
  157. this.serverlessLog(`Offline executing script [${command}]`);
  158. return new Promise(resolve => {
  159. exec(command, (error, stdout, stderr) => {
  160. this.serverlessLog(`exec stdout: [${stdout}]`);
  161. this.serverlessLog(`exec stderr: [${stderr}]`);
  162. if (error) {
  163. // Use the failed command's exit code, proceed as normal so that shutdown can occur gracefully
  164. this.serverlessLog(`Offline error executing script [${error}]`);
  165. this.exitCode = error.code || 1;
  166. }
  167. resolve();
  168. });
  169. });
  170. }
  171. _buildServer() {
  172. // Maps a request id to the request's state (done: bool, timeout: timer)
  173. this.requests = {};
  174. // Methods
  175. this._setOptions(); // Will create meaningful options from cli options
  176. this._storeOriginalEnvironment(); // stores the original process.env for assigning upon invoking the handlers
  177. this._registerBabel(); // Support for ES6
  178. this._createServer(); // Hapijs boot
  179. this._createRoutes(); // API Gateway emulation
  180. this._createResourceRoutes(); // HTTP Proxy defined in Resource
  181. this._create404Route(); // Not found handling
  182. return this.server;
  183. }
  184. _storeOriginalEnvironment() {
  185. this.originalEnvironment = _.extend({}, process.env);
  186. }
  187. _setOptions() {
  188. // Merge the different sources of values for this.options
  189. // Precedence is: command line options, YAML options, defaults.
  190. const defaultOpts = {
  191. host: 'localhost',
  192. location: '.',
  193. port: 3000,
  194. prefix: '/',
  195. stage: this.service.provider.stage,
  196. region: this.service.provider.region,
  197. noTimeout: false,
  198. noEnvironment: false,
  199. resourceRoutes: false,
  200. dontPrintOutput: false,
  201. httpsProtocol: '',
  202. skipCacheInvalidation: false,
  203. noAuth: false,
  204. corsAllowOrigin: '*',
  205. corsAllowHeaders: 'accept,content-type,x-api-key',
  206. corsAllowCredentials: true,
  207. apiKey: crypto.createHash('md5').digest('hex'),
  208. };
  209. this.options = _.merge({}, defaultOpts, (this.service.custom || {})['serverless-offline'], this.options);
  210. // Prefix must start and end with '/'
  211. if (!this.options.prefix.startsWith('/')) this.options.prefix = `/${this.options.prefix}`;
  212. if (!this.options.prefix.endsWith('/')) this.options.prefix += '/';
  213. this.globalBabelOptions = ((this.service.custom || {})['serverless-offline'] || {}).babelOptions;
  214. this.velocityContextOptions = {
  215. stageVariables: {}, // this.service.environment.stages[this.options.stage].vars,
  216. stage: this.options.stage,
  217. };
  218. // Parse CORS options
  219. this.options.corsAllowOrigin = this.options.corsAllowOrigin.replace(/\s/g, '').split(',');
  220. this.options.corsAllowHeaders = this.options.corsAllowHeaders.replace(/\s/g, '').split(',');
  221. if (this.options.corsDisallowCredentials) this.options.corsAllowCredentials = false;
  222. this.options.corsConfig = {
  223. origin: this.options.corsAllowOrigin,
  224. headers: this.options.corsAllowHeaders,
  225. credentials: this.options.corsAllowCredentials,
  226. };
  227. this.serverlessLog(`Starting Offline: ${this.options.stage}/${this.options.region}.`);
  228. debugLog('options:', this.options);
  229. debugLog('globalBabelOptions:', this.globalBabelOptions);
  230. }
  231. _registerBabel(isBabelRuntime, babelRuntimeOptions) {
  232. const options = isBabelRuntime ?
  233. babelRuntimeOptions || { presets: ['es2015'] } :
  234. this.globalBabelOptions;
  235. if (options) {
  236. debugLog('Setting babel register:', options);
  237. // We invoke babel-register only once
  238. if (!this.babelRegister) {
  239. debugLog('For the first time');
  240. this.babelRegister = require('babel-register')(options);
  241. }
  242. }
  243. }
  244. _createServer() {
  245. // Hapijs server creation
  246. this.server = new Hapi.Server({
  247. connections: {
  248. router: {
  249. stripTrailingSlash: true, // removes trailing slashes on incoming paths.
  250. },
  251. },
  252. });
  253. this.server.register(require('h2o2'), err => err && this.serverlessLog(err));
  254. const connectionOptions = {
  255. host: this.options.host,
  256. port: this.options.port,
  257. };
  258. const httpsDir = this.options.httpsProtocol;
  259. // HTTPS support
  260. if (typeof httpsDir === 'string' && httpsDir.length > 0) {
  261. connectionOptions.tls = {
  262. key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'),
  263. cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'),
  264. };
  265. }
  266. // Passes the configuration object to the server
  267. this.server.connection(connectionOptions);
  268. // Enable CORS preflight response
  269. this.server.ext('onPreResponse', corsHeaders);
  270. }
  271. _createRoutes() {
  272. const defaultContentType = 'application/json';
  273. const serviceRuntime = this.service.provider.runtime;
  274. const apiKeys = this.service.provider.apiKeys;
  275. const protectedRoutes = [];
  276. if (['nodejs', 'nodejs4.3', 'nodejs6.10', 'babel'].indexOf(serviceRuntime) === -1) {
  277. this.printBlankLine();
  278. this.serverlessLog(`Warning: found unsupported runtime '${serviceRuntime}'`);
  279. return;
  280. }
  281. // for simple API Key authentication model
  282. if (!_.isEmpty(apiKeys)) {
  283. this.serverlessLog(`Key with token: ${this.options.apiKey}`);
  284. this.serverlessLog('Remember to use x-api-key on the request headers');
  285. }
  286. Object.keys(this.service.functions).forEach(key => {
  287. const fun = this.service.getFunction(key);
  288. const funName = key;
  289. const servicePath = path.join(this.serverless.config.servicePath, this.options.location);
  290. const funOptions = functionHelper.getFunctionOptions(fun, key, servicePath);
  291. debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `);
  292. this.printBlankLine();
  293. debugLog(funName, 'runtime', serviceRuntime, funOptions.babelOptions || '');
  294. this.serverlessLog(`Routes for ${funName}:`);
  295. // Adds a route for each http endpoint
  296. (fun.events && fun.events.length || this.serverlessLog('(none)')) && fun.events.forEach(event => {
  297. if (!event.http) return this.serverlessLog('(none)');
  298. // Handle Simple http setup, ex. - http: GET users/index
  299. if (typeof event.http === 'string') {
  300. const split = event.http.split(' ');
  301. event.http = {
  302. path: split[1],
  303. method: split[0],
  304. };
  305. }
  306. if (_.eq(event.http.private, true)) {
  307. protectedRoutes.push(`${event.http.method.toUpperCase()}#/${event.http.path}`);
  308. }
  309. // generate an enpoint via the endpoint class
  310. const endpoint = new Endpoint(event.http, funOptions).generate();
  311. let firstCall = true;
  312. const integration = endpoint.integration || 'lambda-proxy';
  313. const epath = endpoint.path;
  314. const method = endpoint.method.toUpperCase();
  315. const requestTemplates = endpoint.requestTemplates;
  316. // Prefix must start and end with '/' BUT path must not end with '/'
  317. let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath);
  318. if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1);
  319. fullPath = fullPath.replace(/\+}/g, '*}');
  320. this.serverlessLog(`${method} ${fullPath}`);
  321. // If the endpoint has an authorization function, create an authStrategy for the route
  322. const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath);
  323. let cors = null;
  324. if (endpoint.cors) {
  325. cors = {
  326. origin: endpoint.cors.origins || this.options.corsConfig.origin,
  327. headers: endpoint.cors.headers || this.options.corsConfig.headers,
  328. credentials: endpoint.cors.credentials || this.options.corsConfig.credentials,
  329. };
  330. }
  331. // Route creation
  332. const routeMethod = method === 'ANY' ? '*' : method;
  333. const routeConfig = {
  334. cors,
  335. auth: authStrategyName,
  336. timeout: { socket: false },
  337. };
  338. if (routeMethod !== 'HEAD' && routeMethod !== 'GET') {
  339. // maxBytes: Increase request size from 1MB default limit to 10MB.
  340. // Cf AWS API GW payload limits.
  341. routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 };
  342. }
  343. this.server.route({
  344. method: routeMethod,
  345. path: fullPath,
  346. config: routeConfig,
  347. handler: (request, reply) => { // Here we go
  348. // Payload processing
  349. request.payload = request.payload && request.payload.toString();
  350. request.rawPayload = request.payload;
  351. // Headers processing
  352. // Hapi lowercases the headers whereas AWS does not
  353. // so we recreate a custom headers object from the raw request
  354. const headersArray = request.raw.req.rawHeaders;
  355. // During tests, `server.inject` uses *shot*, a package
  356. // for performing injections that does not entirely mimick
  357. // Hapi's usual request object. rawHeaders are then missing
  358. // Hence the fallback for testing
  359. // Normal usage
  360. if (headersArray) {
  361. const unprocessedHeaders = {};
  362. for (let i = 0; i < headersArray.length; i += 2) {
  363. unprocessedHeaders[headersArray[i]] = headersArray[i + 1];
  364. }
  365. request.unprocessedHeaders = unprocessedHeaders;
  366. }
  367. // Lib testing
  368. else {
  369. request.unprocessedHeaders = request.headers;
  370. // console.log('request.unprocessedHeaders:', request.unprocessedHeaders);
  371. }
  372. // Incomming request message
  373. this.printBlankLine();
  374. this.serverlessLog(`${method} ${request.path} (λ: ${funName})`);
  375. if (firstCall) {
  376. this.serverlessLog('The first request might take a few extra seconds');
  377. firstCall = false;
  378. }
  379. // this.serverlessLog(protectedRoutes);
  380. // Check for APIKey
  381. if (_.includes(protectedRoutes, `${routeMethod}#${fullPath}`) || _.includes(protectedRoutes, `ANY#${fullPath}`)) {
  382. const errorResponse = response => response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException');
  383. if ('x-api-key' in request.headers) {
  384. const requestToken = request.headers['x-api-key'];
  385. if (requestToken !== this.options.apiKey) {
  386. debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`);
  387. return errorResponse(reply);
  388. }
  389. }
  390. else {
  391. debugLog(`Missing x-api-key on private function ${funName}`);
  392. return errorResponse(reply);
  393. }
  394. }
  395. // Shared mutable state is the root of all evil they say
  396. const requestId = Math.random().toString().slice(2);
  397. this.requests[requestId] = { done: false };
  398. this.currentRequestId = requestId;
  399. // Holds the response to do async op
  400. const response = reply.response().hold();
  401. const contentType = request.mime || defaultContentType;
  402. // default request template to '' if we don't have a definition pushed in from serverless or endpoint
  403. const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : '';
  404. // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing
  405. // so we have to do it ourselves
  406. const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json'];
  407. if (contentTypesThatRequirePayloadParsing.indexOf(contentType) !== -1) {
  408. try {
  409. request.payload = JSON.parse(request.payload);
  410. }
  411. catch (err) {
  412. debugLog('error in converting request.payload to JSON:', err);
  413. }
  414. }
  415. debugLog('requestId:', requestId);
  416. debugLog('contentType:', contentType);
  417. debugLog('requestTemplate:', requestTemplate);
  418. debugLog('payload:', request.payload);
  419. /* HANDLER LAZY LOADING */
  420. let handler; // The lambda function
  421. try {
  422. process.env = _.extend({}, this.service.provider.environment, this.service.functions[key].environment, this.originalEnvironment);
  423. handler = functionHelper.createHandler(funOptions, this.options);
  424. }
  425. catch (err) {
  426. return this._reply500(response, `Error while loading ${funName}`, err, requestId);
  427. }
  428. /* REQUEST TEMPLATE PROCESSING (event population) */
  429. let event = {};
  430. if (integration === 'lambda') {
  431. if (requestTemplate) {
  432. try {
  433. debugLog('_____ REQUEST TEMPLATE PROCESSING _____');
  434. // Velocity templating language parsing
  435. const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {});
  436. event = renderVelocityTemplateObject(requestTemplate, velocityContext);
  437. }
  438. catch (err) {
  439. return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err, requestId);
  440. }
  441. }
  442. else if (typeof request.payload === 'object') {
  443. event = request.payload || {};
  444. }
  445. }
  446. else if (integration === 'lambda-proxy') {
  447. event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables);
  448. }
  449. event.isOffline = true;
  450. if (this.serverless.service.custom && this.serverless.service.custom.stageVariables) {
  451. event.stageVariables = this.serverless.service.custom.stageVariables;
  452. }
  453. else if (integration !== 'lambda-proxy') {
  454. event.stageVariables = {};
  455. }
  456. debugLog('event:', event);
  457. // We create the context, its callback (context.done/succeed/fail) will send the HTTP response
  458. const lambdaContext = createLambdaContext(fun, (err, data) => {
  459. // Everything in this block happens once the lambda function has resolved
  460. debugLog('_____ HANDLER RESOLVED _____');
  461. // Timeout clearing if needed
  462. if (this._clearTimeout(requestId)) return;
  463. // User should not call context.done twice
  464. if (this.requests[requestId].done) {
  465. this.printBlankLine();
  466. this.serverlessLog(`Warning: context.done called twice within handler '${funName}'!`);
  467. debugLog('requestId:', requestId);
  468. return;
  469. }
  470. this.requests[requestId].done = true;
  471. let result = data;
  472. let responseName = 'default';
  473. const responseContentType = endpoint.responseContentType;
  474. /* RESPONSE SELECTION (among endpoint's possible responses) */
  475. // Failure handling
  476. let errorStatusCode = 0;
  477. if (err) {
  478. const errorMessage = (err.message || err).toString();
  479. const re = /\[(\d{3})]/;
  480. const found = errorMessage.match(re);
  481. if (found && found.length > 1) {
  482. errorStatusCode = found[1];
  483. }
  484. else {
  485. errorStatusCode = '500';
  486. }
  487. // Mocks Lambda errors
  488. result = {
  489. errorMessage,
  490. errorType: err.constructor.name,
  491. stackTrace: this._getArrayStackTrace(err.stack),
  492. };
  493. this.serverlessLog(`Failure: ${errorMessage}`);
  494. if (result.stackTrace) {
  495. debugLog(result.stackTrace.join('\n '));
  496. }
  497. for (const key in endpoint.responses) {
  498. if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) {
  499. responseName = key;
  500. break;
  501. }
  502. }
  503. }
  504. debugLog(`Using response '${responseName}'`);
  505. const chosenResponse = endpoint.responses[responseName];
  506. /* RESPONSE PARAMETERS PROCCESSING */
  507. const responseParameters = chosenResponse.responseParameters;
  508. if (_.isPlainObject(responseParameters)) {
  509. const responseParametersKeys = Object.keys(responseParameters);
  510. debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____');
  511. debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`);
  512. responseParametersKeys.forEach(key => {
  513. // responseParameters use the following shape: "key": "value"
  514. const value = responseParameters[key];
  515. const keyArray = key.split('.'); // eg: "method.response.header.location"
  516. const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url"
  517. debugLog(`Processing responseParameter "${key}": "${value}"`);
  518. // For now the plugin only supports modifying headers
  519. if (key.startsWith('method.response.header') && keyArray[3]) {
  520. const headerName = keyArray.slice(3).join('.');
  521. let headerValue;
  522. debugLog('Found header in left-hand:', headerName);
  523. if (value.startsWith('integration.response')) {
  524. if (valueArray[2] === 'body') {
  525. debugLog('Found body in right-hand');
  526. headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString();
  527. }
  528. else {
  529. this.printBlankLine();
  530. this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`);
  531. this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`);
  532. this.logPluginIssue();
  533. this.printBlankLine();
  534. }
  535. }
  536. else {
  537. headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34
  538. }
  539. // Applies the header;
  540. debugLog(`Will assign "${headerValue}" to header "${headerName}"`);
  541. response.header(headerName, headerValue);
  542. }
  543. else {
  544. this.printBlankLine();
  545. this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`);
  546. this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`);
  547. this.logPluginIssue();
  548. this.printBlankLine();
  549. }
  550. });
  551. }
  552. let statusCode = 200;
  553. if (integration === 'lambda') {
  554. /* RESPONSE TEMPLATE PROCCESSING */
  555. // If there is a responseTemplate, we apply it to the result
  556. const responseTemplates = chosenResponse.responseTemplates;
  557. if (_.isPlainObject(responseTemplates)) {
  558. const responseTemplatesKeys = Object.keys(responseTemplates);
  559. if (responseTemplatesKeys.length) {
  560. // BAD IMPLEMENTATION: first key in responseTemplates
  561. const responseTemplate = responseTemplates[responseContentType];
  562. if (responseTemplate && responseTemplate !== '\n') {
  563. debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____');
  564. debugLog(`Using responseTemplate '${responseContentType}'`);
  565. try {
  566. const reponseContext = createVelocityContext(request, this.velocityContextOptions, result);
  567. result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root;
  568. }
  569. catch (error) {
  570. this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`);
  571. console.log(error.stack);
  572. }
  573. }
  574. }
  575. }
  576. /* HAPIJS RESPONSE CONFIGURATION */
  577. statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200);
  578. if (!chosenResponse.statusCode) {
  579. this.printBlankLine();
  580. this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`);
  581. }
  582. response.header('Content-Type', responseContentType, {
  583. override: false, // Maybe a responseParameter set it already. See #34
  584. });
  585. response.statusCode = statusCode;
  586. response.source = result;
  587. }
  588. else if (integration === 'lambda-proxy') {
  589. response.statusCode = statusCode = result.statusCode || 200;
  590. const defaultHeaders = { 'Content-Type': 'application/json' };
  591. Object.assign(response.headers, defaultHeaders, result.headers);
  592. if (!_.isUndefined(result.body)) {
  593. if(result.isBase64Encoded) {
  594. response.encoding = 'binary';
  595. response.source = new Buffer(result.body,'base64');
  596. response.variety = 'buffer';
  597. }
  598. else {
  599. response.source = result.body;
  600. }
  601. }
  602. }
  603. // Log response
  604. let whatToLog = result;
  605. try {
  606. whatToLog = JSON.stringify(result);
  607. }
  608. catch (error) {
  609. // nothing
  610. }
  611. finally {
  612. if (!this.options.dontPrintOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`);
  613. debugLog('requestId:', requestId);
  614. }
  615. // Bon voyage!
  616. response.send();
  617. });
  618. // Now we are outside of createLambdaContext, so this happens before the handler gets called:
  619. // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves
  620. this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout(
  621. this._replyTimeout.bind(this, response, funName, funOptions.funTimeout, requestId),
  622. funOptions.funTimeout
  623. );
  624. // Finally we call the handler
  625. debugLog('_____ CALLING HANDLER _____');
  626. try {
  627. const x = handler(event, lambdaContext, lambdaContext.done);
  628. // Promise support
  629. if (serviceRuntime === 'babel' && !this.requests[requestId].done) {
  630. if (x && typeof x.then === 'function' && typeof x.catch === 'function') x.then(lambdaContext.succeed).catch(lambdaContext.fail);
  631. else if (x instanceof Error) lambdaContext.fail(x);
  632. }
  633. }
  634. catch (error) {
  635. return this._reply500(response, `Uncaught error in your '${funName}' handler`, error, requestId);
  636. }
  637. },
  638. });
  639. });
  640. });
  641. }
  642. _configureAuthorization(endpoint, funName, method, epath, servicePath) {
  643. let authStrategyName = null;
  644. if (endpoint.authorizer) {
  645. let authFunctionName = endpoint.authorizer;
  646. if (typeof authFunctionName === 'string' && authFunctionName.toUpperCase() === 'AWS_IAM') {
  647. this.serverlessLog('WARNING: Serverless Offline does not support the AWS_IAM authorization type');
  648. return null;
  649. }
  650. if (typeof endpoint.authorizer === 'object') {
  651. if (endpoint.authorizer.type && endpoint.authorizer.type.toUpperCase() === 'AWS_IAM') {
  652. this.serverlessLog('WARNING: Serverless Offline does not support the AWS_IAM authorization type');
  653. return null;
  654. }
  655. if (endpoint.authorizer.arn) {
  656. this.serverlessLog(`WARNING: Serverless Offline does not support non local authorizers: ${endpoint.authorizer.arn}`);
  657. return authStrategyName;
  658. }
  659. authFunctionName = endpoint.authorizer.name;
  660. }
  661. this.serverlessLog(`Configuring Authorization: ${endpoint.path} ${authFunctionName}`);
  662. const authFunction = this.service.getFunction(authFunctionName);
  663. if (!authFunction) return this.serverlessLog(`WARNING: Authorization function ${authFunctionName} does not exist`);
  664. const authorizerOptions = {
  665. resultTtlInSeconds: '300',
  666. identitySource: 'method.request.header.Authorization',
  667. };
  668. if (typeof endpoint.authorizer === 'string') {
  669. authorizerOptions.name = authFunctionName;
  670. }
  671. else {
  672. Object.assign(authorizerOptions, endpoint.authorizer);
  673. }
  674. // Create a unique scheme per endpoint
  675. // This allows the methodArn on the event property to be set appropriately
  676. const authKey = `${funName}-${authFunctionName}-${method}-${epath}`;
  677. const authSchemeName = `scheme-${authKey}`;
  678. authStrategyName = `strategy-${authKey}`; // set strategy name for the route config
  679. debugLog(`Creating Authorization scheme for ${authKey}`);
  680. // Create the Auth Scheme for the endpoint
  681. const scheme = createAuthScheme(
  682. authFunction,
  683. authorizerOptions,
  684. funName,
  685. epath,
  686. this.options,
  687. this.serverlessLog,
  688. servicePath,
  689. this.serverless
  690. );
  691. // Set the auth scheme and strategy on the server
  692. this.server.auth.scheme(authSchemeName, scheme);
  693. this.server.auth.strategy(authStrategyName, authSchemeName);
  694. }
  695. return authStrategyName;
  696. }
  697. // All done, we can listen to incomming requests
  698. _listen() {
  699. return new Promise((resolve, reject) => {
  700. this.server.start(err => {
  701. if (err) return reject(err);
  702. this.printBlankLine();
  703. this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port}`);
  704. resolve(this.server);
  705. });
  706. });
  707. }
  708. end() {
  709. this.serverlessLog('Halting offline server');
  710. this.server.stop({ timeout: 5000 })
  711. .then(() => process.exit(this.exitCode));
  712. }
  713. // Bad news
  714. _reply500(response, message, err, requestId) {
  715. if (this._clearTimeout(requestId)) return;
  716. this.requests[requestId].done = true;
  717. const stackTrace = this._getArrayStackTrace(err.stack);
  718. this.serverlessLog(message);
  719. if (stackTrace && stackTrace.length > 0) {
  720. console.log(stackTrace);
  721. } else {
  722. console.log(err)
  723. }
  724. /* eslint-disable no-param-reassign */
  725. response.statusCode = 200; // APIG replies 200 by default on failures
  726. response.source = {
  727. errorMessage: message,
  728. errorType: err.constructor.name,
  729. stackTrace,
  730. offlineInfo: 'If you believe this is an issue with the plugin please submit it, thanks. https://github.com/dherault/serverless-offline/issues',
  731. };
  732. /* eslint-enable no-param-reassign */
  733. this.serverlessLog('Replying error in handler');
  734. response.send();
  735. }
  736. _replyTimeout(response, funName, funTimeout, requestId) {
  737. if (this.currentRequestId !== requestId) return;
  738. this.requests[requestId].done = true;
  739. this.serverlessLog(`Replying timeout after ${funTimeout}ms`);
  740. /* eslint-disable no-param-reassign */
  741. response.statusCode = 503;
  742. response.source = `[Serverless-Offline] Your λ handler '${funName}' timed out after ${funTimeout}ms.`;
  743. /* eslint-enable no-param-reassign */
  744. response.send();
  745. }
  746. _clearTimeout(requestId) {
  747. const timeout = this.requests[requestId].timeout;
  748. if (timeout && timeout._called) return true;
  749. clearTimeout(timeout);
  750. }
  751. _createResourceRoutes() {
  752. if (!this.options.resourceRoutes) return true;
  753. const resourceRoutesOptions = this.options.resourceRoutes;
  754. const resourceRoutes = parseResources(this.service.resources);
  755. if (_.isEmpty(resourceRoutes)) return true;
  756. this.printBlankLine();
  757. this.serverlessLog('Routes defined in resources:');
  758. Object.keys(resourceRoutes).forEach(methodId => {
  759. const resourceRoutesObj = resourceRoutes[methodId];
  760. const path = resourceRoutesObj.path;
  761. const method = resourceRoutesObj.method;
  762. const isProxy = resourceRoutesObj.isProxy;
  763. const proxyUri = resourceRoutesObj.proxyUri;
  764. const pathResource = resourceRoutesObj.pathResource;
  765. if (!isProxy) {
  766. return this.serverlessLog(`WARNING: Only HTTP_PROXY is supported. Path '${pathResource}' is ignored.`);
  767. }
  768. if (`${method}`.toUpperCase() !== 'GET') {
  769. return this.serverlessLog(`WARNING: ${method} proxy is not supported. Path '${pathResource}' is ignored.`);
  770. }
  771. if (!path) {
  772. return this.serverlessLog(`WARNING: Could not resolve path for '${methodId}'.`);
  773. }
  774. const proxyUriOverwrite = resourceRoutesOptions[methodId] || {};
  775. const proxyUriInUse = proxyUriOverwrite.Uri || proxyUri;
  776. if (!proxyUriInUse) {
  777. return this.serverlessLog(`WARNING: Could not load Proxy Uri for '${methodId}'`);
  778. }
  779. this.serverlessLog(`${method} ${pathResource} -> ${proxyUriInUse}`);
  780. this.server.route({
  781. method,
  782. path,
  783. config: { cors: this.options.corsConfig },
  784. handler: (request, reply) => {
  785. const params = request.params;
  786. let resultUri = proxyUriInUse;
  787. Object.keys(params).forEach(key => {
  788. resultUri = resultUri.replace(`{${key}}`, params[key]);
  789. });
  790. reply.proxy({ uri: resultUri });
  791. },
  792. });
  793. });
  794. }
  795. _create404Route() {
  796. // If a {proxy+} route exists, don't conflict with it
  797. if (this.server.match('*', '/{p*}')) return;
  798. this.server.route({
  799. method: '*',
  800. path: '/{p*}',
  801. config: { cors: this.options.corsConfig },
  802. handler: (request, reply) => {
  803. const response = reply({
  804. statusCode: 404,
  805. error: 'Serverless-offline: route not found.',
  806. currentRoute: `${request.method} - ${request.path}`,
  807. existingRoutes: this.server.table()[0].table
  808. .filter(route => route.path !== '/{p*}') // Exclude this (404) route
  809. .sort((a, b) => a.path <= b.path ? -1 : 1) // Sort by path
  810. .map(route => `${route.method} - ${route.path}`), // Human-friendly result
  811. });
  812. response.statusCode = 404;
  813. },
  814. });
  815. }
  816. _getArrayStackTrace(stack) {
  817. if (!stack) return null;
  818. const splittedStack = stack.split('\n');
  819. return splittedStack.slice(0, splittedStack.findIndex(item => item.match(/server.route.handler.createLambdaContext/))).map(line => line.trim());
  820. }
  821. _logAndExit() {
  822. console.log.apply(null, arguments);
  823. process.exit(0);
  824. }
  825. }
  826. // Serverless exits with code 1 when a promise rejection is unhandled. Not AWS.
  827. // Users can still use their own unhandledRejection event though.
  828. process.removeAllListeners('unhandledRejection');
  829. module.exports = Offline;