Skip to content
This repository has been archived by the owner on Sep 23, 2021. It is now read-only.

Commit

Permalink
Generate Request ID #66
Browse files Browse the repository at this point in the history
  • Loading branch information
tripodsan committed Oct 10, 2018
1 parent 13f0ee8 commit 2ea228a
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 22 deletions.
37 changes: 27 additions & 10 deletions src/HelixServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -102,20 +111,28 @@ 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;
}
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) => {
Expand Down
18 changes: 18 additions & 0 deletions src/RequestContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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('/');
Expand All @@ -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() {
Expand Down Expand Up @@ -85,6 +99,10 @@ module.exports = class RequestContext {
return this._headers;
}

get wskHeaders() {
return this._wskHeaders;
}

get method() {
return this._method;
}
Expand Down
27 changes: 27 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
55 changes: 45 additions & 10 deletions test/hlx_server_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
}
Expand Down
3 changes: 3 additions & 0 deletions test/specs/expected_binary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"data": "00112233"
}
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
33 changes: 33 additions & 0 deletions test/specs/local/build/binary_html.js
Original file line number Diff line number Diff line change
@@ -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'),
};
};
5 changes: 4 additions & 1 deletion test/specs/local/build/dump_html.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
*/
module.exports.main = function main(params){
return {
body: JSON.stringify(params, null, " "),
headers: {
'Content-Type': 'application/json',
},
body: params,
};
};
26 changes: 26 additions & 0 deletions test/utils_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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;
}
});
});
});

0 comments on commit 2ea228a

Please sign in to comment.