From 2ea228a903a35986fcf002c0eb6b874036759b46 Mon Sep 17 00:00:00 2001 From: Tobias Bocanegra Date: Wed, 10 Oct 2018 14:49:06 +0900 Subject: [PATCH] Generate Request ID #66 --- src/HelixServer.js | 37 +++++++++---- src/RequestContext.js | 18 ++++++ src/utils.js | 27 +++++++++ test/hlx_server_test.js | 55 +++++++++++++++---- test/specs/expected_binary.json | 3 + ...{expected_dump.html => expected_dump.json} | 6 +- test/specs/local/build/binary_html.js | 33 +++++++++++ test/specs/local/build/dump_html.js | 5 +- test/utils_test.js | 26 +++++++++ 9 files changed, 188 insertions(+), 22 deletions(-) create mode 100644 test/specs/expected_binary.json rename test/specs/{expected_dump.html => expected_dump.json} (72%) create mode 100644 test/specs/local/build/binary_html.js diff --git a/src/HelixServer.js b/src/HelixServer.js index 9dc62f40..89865b7c 100644 --- a/src/HelixServer.js +++ b/src/HelixServer.js @@ -10,9 +10,10 @@ * governing permissions and limitations under the License. */ +const EventEmitter = require('events'); +const { Module } = require('module'); const express = require('express'); const NodeESI = require('nodesi'); -const { Module } = require('module'); const utils = require('./utils.js'); const logger = require('./logger.js'); @@ -55,11 +56,17 @@ function executeTemplate(ctx) { // eslint-disable-next-line import/no-dynamic-require,global-require const mod = require(ctx.templatePath); + // openwhisk uses lowercase header names + const owHeaders = {}; + Object.keys(ctx.wskHeaders).forEach((k) => { + owHeaders[k.toLowerCase()] = ctx.wskHeaders[k]; + }); + Module._nodeModulePaths = nodeModulePathsFn; /* eslint-enable no-underscore-dangle */ return Promise.resolve(mod.main({ - __ow_headers: ctx.headers, - __ow_method: ctx.method, + __ow_headers: owHeaders, + __ow_method: ctx.method.toLowerCase(), __ow_logger: logger, // this causes ow-wrapper to use this logger owner: ctx.config.contentRepo.owner, repo: ctx.config.contentRepo.repo, @@ -71,12 +78,13 @@ function executeTemplate(ctx) { })); } -class HelixServer { +class HelixServer extends EventEmitter { /** * Creates a new HelixServer for the given project. * @param {HelixProject} project */ constructor(project) { + super(); this._project = project; this._app = express(); this._port = DEFAULT_PORT; @@ -90,6 +98,7 @@ class HelixServer { const boundResolver = this._templateResolver.resolve.bind(this._templateResolver); this._app.get('*', (req, res) => { const ctx = new RequestContext(req, this._project); + this.emit('request', req, res, ctx); if (!ctx.valid) { res.status(404).send(); return; @@ -102,6 +111,9 @@ class HelixServer { .then(boundResolver) .then(executeTemplate) .then((result) => { + if (!result) { + throw new Error('Response is empty, don\'t know what to do'); + } if (result instanceof Error) { // full response is an error: engine error throw result; @@ -109,13 +121,18 @@ class HelixServer { if (result && result.error && result.error instanceof Error) { throw result.error; } - if (!result || !result.body) { - // empty body: nothing to render - throw new Error('Response has no body, don\'t know what to do'); - } + let body = result.body || ''; + const headers = result.headers || {}; const status = result.statusCode || 200; - esi.process(result.body).then((body) => { - res.status(status).send(body); + const contentType = headers['Content-Type'] || 'text/html'; + if (/.*\/json/.test(contentType)) { + body = JSON.stringify(body); + } else if (/.*\/octet-stream/.test(contentType) || /image\/.*/.test(contentType)) { + body = Buffer.from(body, 'base64'); + } + res.set(headers); + esi.process(body).then((esiBody) => { + res.status(status).send(esiBody); }); }) .catch((err) => { diff --git a/src/RequestContext.js b/src/RequestContext.js index fb52496f..8aae615d 100644 --- a/src/RequestContext.js +++ b/src/RequestContext.js @@ -9,6 +9,9 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + +const utils = require('./utils.js'); + /** * Context that is used during request handling. * @@ -25,6 +28,9 @@ module.exports = class RequestContext { this._headers = req.headers || {}; this._method = req.method || 'GET'; this._params = req.query || {}; + this._wskActivationId = utils.randomChars(32, true); + this._requestId = utils.randomChars(32); + this._cdnRequestId = utils.uuid(); let relPath = this._path; const lastSlash = relPath.lastIndexOf('/'); @@ -46,6 +52,14 @@ module.exports = class RequestContext { relPath += 'index'; } this._resourcePath = relPath; + + // generate headers + this._wskHeaders = Object.assign({ + 'X-Openwhisk-Activation-Id': this._wskActivationId, + 'X-Request-Id': this._requestId, + 'X-Backend-Name': 'localhost--F_Petridish', + 'X-CDN-Request-Id': this._cdnRequestId, + }, this._headers); } get url() { @@ -85,6 +99,10 @@ module.exports = class RequestContext { return this._headers; } + get wskHeaders() { + return this._wskHeaders; + } + get method() { return this._method; } diff --git a/src/utils.js b/src/utils.js index 655ebf69..5624ed2d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -12,6 +12,7 @@ const fs = require('fs-extra'); const request = require('request-promise'); const path = require('path'); +const crypto = require('crypto'); const logger = require('./logger.js'); const utils = { @@ -90,6 +91,32 @@ const utils = { throw error; }, + /** + * Generates a random string of the given `length` consisting of alpha numerical characters. + * if `hex` is {@code true}, the string will only consist of hexadecimal digits. + * @param {number}length length of the string. + * @param {boolean} hex returns a hex string if {@code true} + * @returns {String} a random string. + */ + randomChars(length, hex = false) { + if (length === 0) { + return ''; + } + if (hex) { + return crypto.randomBytes(Math.round(length / 2)).toString('hex').substring(0, length); + } + const str = crypto.randomBytes(length).toString('base64'); + return str.substring(0, length); + }, + + /** + * Generates a completely random uuid of the format: + * `00000000-0000-0000-0000-000000000000` + * @returns {string} A random uuid. + */ + uuid() { + return `${utils.randomChars(8, true)}-${utils.randomChars(4, true)}-${utils.randomChars(4, true)}-${utils.randomChars(4, true)}-${utils.randomChars(12, true)}`; + }, }; module.exports = Object.freeze(utils); diff --git a/test/hlx_server_test.js b/test/hlx_server_test.js index 55117e41..22d6ffb4 100644 --- a/test/hlx_server_test.js +++ b/test/hlx_server_test.js @@ -28,6 +28,8 @@ if (!shell.which('git')) { // throw a Javascript error when any shell.js command encounters an error shell.config.fatal = true; +const _isFunction = fn => !!(fn && fn.constructor && fn.call && fn.apply); + const SPEC_ROOT = path.resolve(__dirname, 'specs'); const SPECS_WITH_GIT = [ @@ -48,9 +50,9 @@ function removeRepository(dir) { } // todo: use replay ? -async function assertHttp(url, status, spec, port, gitPort) { +async function assertHttp(url, status, spec, subst) { return new Promise((resolve, reject) => { - let data = ''; + const data = []; http.get(url, (res) => { try { assert.equal(res.statusCode, status); @@ -61,19 +63,27 @@ async function assertHttp(url, status, spec, port, gitPort) { res .on('data', (chunk) => { - data += chunk; + data.push(chunk); }) .on('end', () => { try { if (spec) { + const dat = Buffer.concat(data); let expected = fse.readFileSync(path.resolve(__dirname, 'specs', spec)).toString(); - if (port) { - expected = expected.replace(/SERVER_PORT/g, port); - } - if (gitPort) { - expected = expected.replace(/GIT_PORT/g, gitPort); + const repl = (_isFunction(subst) ? subst() : subst) || {}; + Object.keys(repl).forEach((k) => { + const reg = new RegExp(k, 'g'); + expected = expected.replace(reg, repl[k]); + }); + if (/\/json/.test(res.headers['content-type'])) { + assert.deepEqual(JSON.parse(dat), JSON.parse(expected)); + } else if (/octet-stream/.test(res.headers['content-type'])) { + expected = JSON.parse(expected).data; + const actual = dat.toString('hex'); + assert.equal(actual, expected); + } else { + assert.equal(data.toString().trim(), expected.trim()); } - assert.equal(data.trim(), expected.trim()); } resolve(); } catch (e) { @@ -121,7 +131,32 @@ describe('Helix Server', () => { await project.init(); try { await project.start(); - await assertHttp(`http://localhost:${project.server.port}/index.dump.html`, 200, 'expected_dump.html', project.server.port, project.gitState.httpPort); + let reqCtx = null; + project.server.on('request', (req, res, ctx) => { + reqCtx = ctx; + }); + await assertHttp(`http://localhost:${project.server.port}/index.dump.html`, 200, 'expected_dump.json', () => ({ + SERVER_PORT: project.server.port, + GIT_PORT: project.gitState.httpPort, + X_WSK_ACTIVATION_ID: reqCtx._wskActivationId, + X_REQUEST_ID: reqCtx._requestId, + X_CDN_REQUEST_ID: reqCtx._cdnRequestId, + })); + } finally { + await project.stop(); + } + }); + + it('deliver binary data', async () => { + const cwd = path.join(SPEC_ROOT, 'local'); + const project = new HelixProject() + .withCwd(cwd) + .withBuildDir('./build') + .withHttpPort(0); + await project.init(); + try { + await project.start(); + await assertHttp(`http://localhost:${project.server.port}/index.binary.html`, 200, 'expected_binary.json'); } finally { await project.stop(); } diff --git a/test/specs/expected_binary.json b/test/specs/expected_binary.json new file mode 100644 index 00000000..3147cb11 --- /dev/null +++ b/test/specs/expected_binary.json @@ -0,0 +1,3 @@ +{ + "data": "00112233" +} diff --git a/test/specs/expected_dump.html b/test/specs/expected_dump.json similarity index 72% rename from test/specs/expected_dump.html rename to test/specs/expected_dump.json index 3f5ac89d..776b4a36 100644 --- a/test/specs/expected_dump.html +++ b/test/specs/expected_dump.json @@ -1,9 +1,13 @@ { "__ow_headers": { + "x-openwhisk-activation-id": "X_WSK_ACTIVATION_ID", + "x-request-id": "X_REQUEST_ID", + "x-backend-name": "localhost--F_Petridish", + "x-cdn-request-id": "X_CDN_REQUEST_ID", "host": "localhost:SERVER_PORT", "connection": "close" }, - "__ow_method": "GET", + "__ow_method": "get", "__ow_logger": {}, "owner": "helix", "repo": "content", diff --git a/test/specs/local/build/binary_html.js b/test/specs/local/build/binary_html.js new file mode 100644 index 00000000..0d3236a1 --- /dev/null +++ b/test/specs/local/build/binary_html.js @@ -0,0 +1,33 @@ +/* + * Copyright 2018 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-disable */ + +/* + * Copyright 2018 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +module.exports.main = function main(){ + return { + headers: { + 'Content-Type': 'application/octet-stream', + }, + body: Buffer.from('00112233', 'hex'), + }; +}; diff --git a/test/specs/local/build/dump_html.js b/test/specs/local/build/dump_html.js index 666c59cc..893b70d5 100644 --- a/test/specs/local/build/dump_html.js +++ b/test/specs/local/build/dump_html.js @@ -25,6 +25,9 @@ */ module.exports.main = function main(params){ return { - body: JSON.stringify(params, null, " "), + headers: { + 'Content-Type': 'application/json', + }, + body: params, }; }; diff --git a/test/utils_test.js b/test/utils_test.js index 170c4ba9..93ec83eb 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -14,6 +14,7 @@ const assert = require('assert'); const RequestContext = require('../src/RequestContext.js'); +const utils = require('../src/utils.js'); describe('Utils Test', () => { describe('Request context', () => { @@ -87,4 +88,29 @@ describe('Utils Test', () => { }); }); }); + + describe('Random chars', () => { + it('generates a random string of the desired length', () => { + const generated = {}; + for (let i = 0; i < 32; i += 1) { + const s = utils.randomChars(i); + assert.equal(s.length, i); + assert.ok(!generated[s]); + generated[s] = true; + } + }); + + it('generates a random hex string of the desired length', () => { + const generated = {}; + for (let i = 0; i < 32; i += 1) { + const s = utils.randomChars(i, true); + if (i > 0) { + assert.ok(/^[0-9a-f]+$/.test(s)); + } + assert.equal(s.length, i); + assert.ok(!generated[s]); + generated[s] = true; + } + }); + }); });