From e5dedd7a4e48e213b9873426bcfbd586b6229719 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Fri, 25 Aug 2017 21:22:09 -0400 Subject: [PATCH 01/23] [api] [refactor] Removed STDERR overload * Clears up non-standard usage of STDERR * Remove legacy error handling logic * Now using STDIO 3 for JSON messages * Adds option for service.jail argument --- bin/binaries/micro-node | 51 ++++++++++++------------------- lib/plugins/spawn/index.js | 30 +++++++++++++----- lib/plugins/spawn/stderr/index.js | 42 +++++++++---------------- 3 files changed, 56 insertions(+), 67 deletions(-) diff --git a/bin/binaries/micro-node b/bin/binaries/micro-node index fbab039..5a473ac 100755 --- a/bin/binaries/micro-node +++ b/bin/binaries/micro-node @@ -52,6 +52,8 @@ try { } +// pipe3 is an additional STDIO pipe we can use for HTTP response methods, debugging events, logging events, and other out of band comms +var pipe3 = new net.Socket({ fd: 3 }); // babel support for es6 / es7 // the spawned child needs regenerator run-time here @@ -84,7 +86,7 @@ output.addTrailers = function (headers) { "headers": headers } }; - console.error(JSON.stringify(message)); + pipe3.write(JSON.stringify(message)); }; output.removeHeader = function (name) { @@ -94,7 +96,7 @@ output.removeHeader = function (name) { "name": name } }; - console.error(JSON.stringify(message)); + pipe3.write(JSON.stringify(message)); }; output.setHeader = function (name, value) { @@ -105,7 +107,7 @@ output.setHeader = function (name, value) { "value": value } }; - console.error(JSON.stringify(message)); + pipe3.write(JSON.stringify(message)); }; output.setTimeout = function (msecs, cb) { @@ -116,7 +118,7 @@ output.setTimeout = function (msecs, cb) { "msecs": msecs } }; - console.error(JSON.stringify(message)); + pipe3.write(JSON.stringify(message)); }; output.sendDate = function (value) { @@ -126,7 +128,7 @@ output.sendDate = function (value) { "value": value } }; - console.error(JSON.stringify(message)); + pipe3.write(JSON.stringify(message)); }; output.statusMessage = function (value) { @@ -136,7 +138,7 @@ output.statusMessage = function (value) { "value": value } }; - console.error(JSON.stringify(message)); + pipe3.write(JSON.stringify(message)); }; // Using Object.defineProperty @@ -148,7 +150,7 @@ Object.defineProperty(output, 'statusCode', { "value": value } }; - console.error(JSON.stringify(message)); + pipe3.write(JSON.stringify(message)); } }); @@ -158,7 +160,7 @@ output.writeContinue = function () { "payload": { } }; - console.error(JSON.stringify(message)); + pipe3.write(JSON.stringify(message)); }; output.writeHead = function (code, headers) { @@ -169,12 +171,12 @@ output.writeHead = function (code, headers) { "headers": headers } }; - console.error(JSON.stringify(message)); + pipe3.write(JSON.stringify(message)); }; // Capture any stream errors -output.on('error', function(err){ - console.error(JSON.stringify({ type: "error", payload: { error: err.message, code: err.code } })); +output.on('error', function (err) { + pipe3.write(JSON.stringify({ type: "error", payload: { error: err.message, code: err.code } })); process.exit(); }); @@ -184,7 +186,7 @@ output.json = function json (data) { if (typeof data !== 'undefined') { console.log(JSON.stringify(data, true, 2)); } - console.error(JSON.stringify({ type: "end" })); + pipe3.write(JSON.stringify({ type: "end" })); process.exit(); }; @@ -193,14 +195,14 @@ output.end = function end (data) { if (typeof data !== 'undefined') { console.log(data); } - console.error(JSON.stringify({ type: "end" })); + pipe3.write(JSON.stringify({ type: "end" })); process.exit(); }; // Custom errorHandler for `run-service` execution function errorHandler (err) { if (err) { - console.error(JSON.stringify({ type: "error", payload: { error: err.message, code: err.code } })); + pipe3.write(JSON.stringify({ type: "error", payload: { error: err.message, code: err.code } })); process.exit(); } }; @@ -231,7 +233,6 @@ process.stdin.url = env.input.url; process.stdin.connection = env.input.connection; process.stdin.resource = { params: {}}; - // Send logs to stderr as JSON message var debug = function debug () { var args = []; @@ -241,7 +242,7 @@ var debug = function debug () { if (args.length === 1) { args = args[0]; } - console.error(JSON.stringify({ type: "log", payload: { entry: args } })); + pipe3.write(JSON.stringify({ type: "log", payload: { entry: args } })); return; }; @@ -294,20 +295,6 @@ psr(process.stdin, output, function (req, res, fields) { } */ -/* Remark: Removed for now, not needed yet? -// pipe3 is an additional STDIO pipe we can use for HTTP request / response methods -// it's currently being used to handle incoming request.end and request.close events -var pipe3 = new net.Socket({ fd: 3 }); -pipe3.on('data', function (buf) { - var str = buf.toString(); - if (str.search('input.close') !== -1) { - process.stdin.emit('close'); - } - if (str.search('input.end') !== -1) { - process.stdin.emit('end'); - } -}); -*/ /* TODO: add ability to proxy request parameters to middleware chain var proxy = new Proxy(process.stdin, { @@ -323,7 +310,7 @@ var proxy = new Proxy(process.stdin, { //console.log("Setting property '" + name + "', initial value: " + value); if (!(name in target)) { // console.log("Setting non-existant property '" + name + "', initial value: " + value); - console.error(JSON.stringify({ type: "setvar", payload: { key: name, value: value } })); + pipe3.write(JSON.stringify({ type: "setvar", payload: { key: name, value: value } })); } target[name] = value; return true; @@ -334,7 +321,7 @@ var proxy = new Proxy(process.stdin, { /* TODO: add ability to send proxied params back to parent process // sets key value on input stream ( useful for middleware processing later ) process.stdin.set = function (key, value) { - console.error(JSON.stringify({ type: "setvar", payload: { key: key, value: value } })); + pipe3.write(JSON.stringify({ type: "setvar", payload: { key: key, value: value } })); }; */ diff --git a/lib/plugins/spawn/index.js b/lib/plugins/spawn/index.js index 052e6e2..5d49c31 100644 --- a/lib/plugins/spawn/index.js +++ b/lib/plugins/spawn/index.js @@ -374,11 +374,17 @@ module['exports'] = function spawnService (service) { targetBinary = path.normalize(targetBinary); preprocessCommandLineArguments(); - // process.cwd(), + // jail option is used to add a pre-process command to the target binary + // in most expected cases this will be `chroot` or `nsjail` with arguments + if (service.jail) { + binaryArgs.unshift(targetBinary); + binaryArgs = service.jailArgs.concat(binaryArgs); + targetBinary = service.jail; + } + // console.log('spawning', targetBinary, 'in', _service.cwd, 'with', binaryArgs) vm = spawn(targetBinary, binaryArgs, { stdio: ['pipe', 'pipe', 'pipe', 'pipe'], cwd: _service.cwd }); - /* // used for additional communication outside of STDIN / STDOUT / STDERR // pipe3 is additional HTTP req / res methods var pipe3 = vm.stdio[3]; @@ -395,17 +401,24 @@ module['exports'] = function spawnService (service) { // useful for pipe3, but not really being used pipe3.on('end', function(){ + // console.log('pipe3 ended') status.pipe3ended = true; }); pipe3.on('close', function(){ + // console.log('pipe3 close') status.pipe3ended = true; }); + pipe3.on('data', function (data) { + // console.log('pipe3 data', data.toString()); + stderr.onData(data, status, log, output, input); + }); + pipe3.on('exit', function(){ + // console.log('pipe3 exit') status.pipe3ended = true; }); - */ finish(); @@ -451,7 +464,7 @@ module['exports'] = function spawnService (service) { serviceCompletedTimer = clearTimeout(serviceCompletedTimer); serviceCompleted = true; - // console.log('endResponse', status, next) + // console.log('endResponse', status) // Note: Only certain languages are currently capable of acting as middlewares // For additional language support, we need an explcit event / API in each language for closing event over STDERR ( same as JS works ) @@ -472,8 +485,8 @@ module['exports'] = function spawnService (service) { if (vm.stdout) { vm.stdout.on('data', function (data) { + output.write(data); if (!status.ended && output.finished !== true) { - output.write(data); } }); } @@ -538,8 +551,9 @@ module['exports'] = function spawnService (service) { // stderr is overloaded here to be used as a one-way messaging device from child process to request // this is used for doing such events as logging / setting http headers vm.stderr.on('data', function (data) { - // console.log('vm.stderr.data', data.toString()) - stderr.onData(data, status, log, output, input); + // console.log('vm.stderr.data', data.toString()); + log(data.toString()); + // stderr.onData(data, status, log, output, input); }); } @@ -582,7 +596,7 @@ module['exports'] = function spawnService (service) { // we must wait for vm.stdout.end and vm.stderr to finish ( as to not lose data ) } // if stdout has ended, we should be able to end the response if the vm exits - if (status.stdoutEnded && !status.ended) { + if (status.stdoutEnded /*&& !status.ended*/) { status.ended = true; // Remark: The vm has exited ( and it's still not ended ) // The service has ended but the VM end event may not have fired, we should attempt to end response diff --git a/lib/plugins/spawn/stderr/index.js b/lib/plugins/spawn/stderr/index.js index 5b16264..72962fc 100644 --- a/lib/plugins/spawn/stderr/index.js +++ b/lib/plugins/spawn/stderr/index.js @@ -64,9 +64,7 @@ var handleMessage = stderr.handleMessage = function (message, status, debug, out } /* - TODO: implement req param setters for middlewares - if (message.type === "setvar") { console.log('calling setvar...', message.payload) input[message.payload.key] = message.payload.value; @@ -76,33 +74,23 @@ var handleMessage = stderr.handleMessage = function (message, status, debug, out // if the incoming message is an error if (message.type === "error") { - // let's do some custom behavior for MODULE_NOT_FOUND errors, - // i.e. require('colors') when colors is not installed status.erroring = true; - // TODO: make module install a hookable event... - if (message.payload.code === "MODULE_NOT_FOUND") { - // TODO: Create module missing event that our package auto-install code can hook into + + // we don't know what happened at this point, or how much more error information is coming + // let's just set a timer to end the request after a few moments + // this ensures that most ( if not the entire ) error stack gets sent to the client + if(!status.ended && output) { + // wait 200 ms to account for any errors to flush output.write(message.payload.error); - status.ended = true; - return output.endResponse(); - } else { - status.erroring = true; - // the process is erroring and its not MODULE_NOT_FOUND. - // we don't know what happened at this point, or how much more error information is coming - // let's just set a timer to end the request after a few moments - // this ensures that most ( if not the entire ) error stack gets sent to the client - if(!status.ended && output) { - // wait 200 ms to account for any errors to flush - output.write(message.payload.error); - status.ended = true; - /* - Removed with new middleware API - Note: Should this be added back? - setTimeout(function(){ - output.end(); - }, 200) - */ - } + status.serviceEnded = true; + /* + Removed with new middleware API + Note: Should this be added back? + // status.ended = true; + setTimeout(function(){ + output.end(); + }, 200) + */ } } } \ No newline at end of file From 88d0db1b06b2512f643ce41881b456bed7beccc5 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Fri, 25 Aug 2017 21:22:31 -0400 Subject: [PATCH 02/23] =?UTF-8?q?[config]=20[fix]=20Don=E2=80=99t=20use=20?= =?UTF-8?q?SSL/HTTPS=20by=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/index.js b/config/index.js index 8684793..46e5a78 100644 --- a/config/index.js +++ b/config/index.js @@ -4,11 +4,11 @@ module.exports = { http: { port: 3000, host: "0.0.0.0", - https: true, // set to `true` to enable SSL server. cert, key, and ca will be required. + https: false, // set to `true` to enable SSL server. cert, key, and ca will be required. key: fs.readFileSync(__dirname + "/ssl/server-key.pem").toString(), cert: fs.readFileSync(__dirname + "/ssl/server-crt.pem").toString(), ca: [fs.readFileSync(__dirname + '/ssl/ca-crt.pem').toString()], - sslRequired: true, // redirects all http traffic to https, optional + sslRequired: false, // redirects all http traffic to https, optional onlySSL: false // will only start https server with no unprotected http interface, optional }, SERVICE_MAX_TIMEOUT: 10000, From 5054f5daac693ef91d5adc04e176017197b26e5d Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Fri, 25 Aug 2017 23:51:36 -0400 Subject: [PATCH 03/23] [api] [fix] Add carriage return to pipe3 writes --- bin/binaries/micro-node | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/binaries/micro-node b/bin/binaries/micro-node index 5a473ac..a058ef0 100755 --- a/bin/binaries/micro-node +++ b/bin/binaries/micro-node @@ -86,7 +86,7 @@ output.addTrailers = function (headers) { "headers": headers } }; - pipe3.write(JSON.stringify(message)); + pipe3.write(JSON.stringify(message) + '\n'); }; output.removeHeader = function (name) { @@ -96,7 +96,7 @@ output.removeHeader = function (name) { "name": name } }; - pipe3.write(JSON.stringify(message)); + pipe3.write(JSON.stringify(message) + '\n'); }; output.setHeader = function (name, value) { @@ -107,7 +107,7 @@ output.setHeader = function (name, value) { "value": value } }; - pipe3.write(JSON.stringify(message)); + pipe3.write(JSON.stringify(message) + '\n'); }; output.setTimeout = function (msecs, cb) { @@ -118,7 +118,7 @@ output.setTimeout = function (msecs, cb) { "msecs": msecs } }; - pipe3.write(JSON.stringify(message)); + pipe3.write(JSON.stringify(message) + '\n'); }; output.sendDate = function (value) { @@ -128,7 +128,7 @@ output.sendDate = function (value) { "value": value } }; - pipe3.write(JSON.stringify(message)); + pipe3.write(JSON.stringify(message) + '\n'); }; output.statusMessage = function (value) { @@ -138,7 +138,7 @@ output.statusMessage = function (value) { "value": value } }; - pipe3.write(JSON.stringify(message)); + pipe3.write(JSON.stringify(message) + '\n'); }; // Using Object.defineProperty @@ -150,7 +150,7 @@ Object.defineProperty(output, 'statusCode', { "value": value } }; - pipe3.write(JSON.stringify(message)); + pipe3.write(JSON.stringify(message) + '\n'); } }); @@ -160,7 +160,7 @@ output.writeContinue = function () { "payload": { } }; - pipe3.write(JSON.stringify(message)); + pipe3.write(JSON.stringify(message) + '\n'); }; output.writeHead = function (code, headers) { @@ -171,7 +171,7 @@ output.writeHead = function (code, headers) { "headers": headers } }; - pipe3.write(JSON.stringify(message)); + pipe3.write(JSON.stringify(message) + '\n'); }; // Capture any stream errors From 109c7bb452d68ca9fb18afd30f830731222785b4 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Wed, 30 Aug 2017 17:21:08 -0400 Subject: [PATCH 04/23] [test] Added invalid services tests * Used to test error conditions and behaviors * Currently only testing errors with JS services * Should cover most code paths for all langauges --- examples/services/invalid/ReadMe.md | 1 - .../invalid-services}/missing-exports.js | 0 .../invalid-services}/never-responds.js | 0 .../invalid-services}/require-error.js | 0 .../invalid-services}/syntax-error.js | 0 .../invalid-services}/writes-bad-headers.js | 0 test/invalid-service-tests.js | 103 ++++++++++++++++++ 7 files changed, 103 insertions(+), 1 deletion(-) delete mode 100644 examples/services/invalid/ReadMe.md rename {examples/services/invalid => test/fixtures/invalid-services}/missing-exports.js (100%) rename {examples/services/invalid => test/fixtures/invalid-services}/never-responds.js (100%) rename {examples/services/invalid => test/fixtures/invalid-services}/require-error.js (100%) rename {examples/services/invalid => test/fixtures/invalid-services}/syntax-error.js (100%) rename {examples/services/invalid => test/fixtures/invalid-services}/writes-bad-headers.js (100%) create mode 100644 test/invalid-service-tests.js diff --git a/examples/services/invalid/ReadMe.md b/examples/services/invalid/ReadMe.md deleted file mode 100644 index 4b36b4f..0000000 --- a/examples/services/invalid/ReadMe.md +++ /dev/null @@ -1 +0,0 @@ -Collection of invalid / erroring microservices. Useful for testing error conditions, can be put into unit tests. \ No newline at end of file diff --git a/examples/services/invalid/missing-exports.js b/test/fixtures/invalid-services/missing-exports.js similarity index 100% rename from examples/services/invalid/missing-exports.js rename to test/fixtures/invalid-services/missing-exports.js diff --git a/examples/services/invalid/never-responds.js b/test/fixtures/invalid-services/never-responds.js similarity index 100% rename from examples/services/invalid/never-responds.js rename to test/fixtures/invalid-services/never-responds.js diff --git a/examples/services/invalid/require-error.js b/test/fixtures/invalid-services/require-error.js similarity index 100% rename from examples/services/invalid/require-error.js rename to test/fixtures/invalid-services/require-error.js diff --git a/examples/services/invalid/syntax-error.js b/test/fixtures/invalid-services/syntax-error.js similarity index 100% rename from examples/services/invalid/syntax-error.js rename to test/fixtures/invalid-services/syntax-error.js diff --git a/examples/services/invalid/writes-bad-headers.js b/test/fixtures/invalid-services/writes-bad-headers.js similarity index 100% rename from examples/services/invalid/writes-bad-headers.js rename to test/fixtures/invalid-services/writes-bad-headers.js diff --git a/test/invalid-service-tests.js b/test/invalid-service-tests.js new file mode 100644 index 0000000..a26b511 --- /dev/null +++ b/test/invalid-service-tests.js @@ -0,0 +1,103 @@ +// invalid-service-tests.js +// attempts to run several user-defined services which may error in unique ways +var test = require("tape"); +var express = require('express'); +var request = require('request'); +var fs = require('fs'); + +var microcule, handlers = {}, app, server; + +test('attempt to require microcule', function (t) { + microcule = require('../'); + t.equal(typeof microcule, 'object', 'microcule module required'); + t.end(); +}); + +test('attempt to create multiple invalid spawn handlers', function (t) { + + handlers['missing-exports'] = microcule.plugins.spawn({ + language: "javascript", + code: fs.readFileSync(__dirname + '/fixtures/invalid-services/missing-exports.js').toString() + }); + + handlers['never-responds'] = microcule.plugins.spawn({ + language: "javascript", + code: fs.readFileSync(__dirname + '/fixtures/invalid-services/never-responds.js').toString(), + customTimeout: 1600 + }); + + handlers['require-error'] = microcule.plugins.spawn({ + language: "javascript", + code: fs.readFileSync(__dirname + '/fixtures/invalid-services/require-error.js').toString() + }); + + handlers['syntax-error'] = microcule.plugins.spawn({ + language: "javascript", + code: fs.readFileSync(__dirname + '/fixtures/invalid-services/syntax-error.js').toString() + }); + + handlers['writes-bad-headers'] = microcule.plugins.spawn({ + language: "javascript", + code: fs.readFileSync(__dirname + '/fixtures/invalid-services/writes-bad-headers.js').toString() + }); + + t.end(); +}); + +test('attempt to start simple http server with multiple invalid services', function (t) { + app = express(); + + app.use('/missing-exports', handlers['missing-exports']); + app.use('/never-responds', handlers['never-responds']); + app.use('/require-error', handlers['require-error']); + app.use('/syntax-error', handlers['syntax-error']); + app.use('/writes-bad-headers', handlers['writes-bad-headers']); + + // Required for non-js services ( or else response will not end ) + app.use(function(req, res){ + res.end(); + }); + + server = app.listen(3000, function () { + t.end(); + }); +}); + +test('attempt to send request to javascript missing-exports service', function (t) { + request('http://localhost:3000/missing-exports', function (err, res, body) { + t.equal(res.statusCode, 500); + t.equal(body, 'service is undefined', 'got correct response'); + t.end(); + }) +}); + +test('attempt to send request to javascript never-responds', function (t) { + request('http://localhost:3000/never-responds', function (err, res, body) { + t.equal(res.statusCode, 500); + t.equal(body.substr(0, 7), 'Timeout', 'got timeout response'); + t.end(); + }) +}); + +test('attempt to send request to javascript require-error', function (t) { + request('http://localhost:3000/require-error', function (err, res, body) { + t.equal(res.statusCode, 500); + t.equal(body, 'a is not defined', 'got correct node error'); + t.end(); + }) +}); + +test('attempt to send request to javascript writes-bad-headers', function (t) { + request('http://localhost:3000/writes-bad-headers', function (err, res, body) { + t.equal(res.statusCode, 200); + t.equal(body, '', 'got correct node error'); + t.end(); + }) +}); + +test('attempt to end server', function (t) { + server.close(function(){ + t.ok("server ended"); + t.end(); + }); +}); From bcd45361e8f5f8f084d57f8244e55cdcfccc0837 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Wed, 30 Aug 2017 21:19:06 -0400 Subject: [PATCH 05/23] [test] Added tests for service as middlewares * Adds tests for chaining services * Covers basic API and usage --- test/service-as-middleware-tests.js | 75 +++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 test/service-as-middleware-tests.js diff --git a/test/service-as-middleware-tests.js b/test/service-as-middleware-tests.js new file mode 100644 index 0000000..19c5cce --- /dev/null +++ b/test/service-as-middleware-tests.js @@ -0,0 +1,75 @@ +// service-as-middleware-tests.js +var test = require("tape"); +var express = require('express'); +var request = require('request'); + +var microcule, handlers = {}, app, server; + +test('attempt to require microcule', function (t) { + microcule = require('../'); + t.equal(typeof microcule, 'object', 'microcule module required'); + t.end(); +}); + +test('attempt to create a few chainable microservice spawn handlers', function (t) { + + handlers['basicAuth'] = microcule.plugins.spawn({ + language: "javascript", + code: function (req, res, next) { + var auth = require('basic-auth') + var credentials = auth(req) + if (!credentials || credentials.name !== 'admin' || credentials.pass !== 'password') { + //res.statusCode(401); + res.setHeader('WWW-Authenticate', 'Basic realm="examples"') + res.writeHead(401); + res.end('Access denied'); + } else { + next(); + } + } + }); + + handlers['write-a'] = microcule.plugins.spawn({ + language: "bash", + code: 'echo "a"' + }); + handlers['write-b'] = microcule.plugins.spawn({ + language: "javascript", + code: function (req, res, next) { + res.write('b\n'); + next(); // call next() to indicate this services is not going to explictly end the response + } + }); + handlers['write-c'] = microcule.plugins.spawn({ + language: "bash", + code: 'echo "c"' + }); + t.end(); +}); + +test('attempt to start simple http server with spawn handler', function (t) { + app = express(); + + app.use([handlers['basicAuth'], handlers['write-a'], handlers['write-b'], handlers['write-c']], function (req, res) { + console.log("No middlewares ended response, made it to end"); + res.end('caught end') + }); + + server = app.listen(3000, function () { + t.end(); + }); +}); + +test('attempt to send simple http request to running microservice', function (t) { + request('http://admin:password@localhost:3000/', function (err, res, body) { + t.equal(body, 'a\nb\nc\ncaught end', 'got correct response'); + t.end(); + }) +}); + +test('attempt to end server', function (t) { + server.close(function(){ + t.ok("server ended"); + t.end(); + }); +}); \ No newline at end of file From 43a49e2616358f90cfab5e1e5cd6f53d03a046b4 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Wed, 30 Aug 2017 21:41:30 -0400 Subject: [PATCH 06/23] [api] Improved spawning API * Makes service homedir configurable * Send 500 status code for timeout errors * Remove legacy code for checking NPM registry * Fix small regression in vm.stdout handler * All tests passing --- lib/plugins/spawn/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/plugins/spawn/index.js b/lib/plugins/spawn/index.js index 5d49c31..430a641 100644 --- a/lib/plugins/spawn/index.js +++ b/lib/plugins/spawn/index.js @@ -62,6 +62,9 @@ module['exports'] = function spawnService (service) { // _service.schema = service.schema; _service.language = service.lang || service.language || "javascript"; + // accept incoming home up to local project root + _service.home = service.home || __dirname + "/../../../"; // /* service.home || */ + // legacy API if (typeof service.source === 'string') { _service.code = service.source; @@ -369,7 +372,7 @@ module['exports'] = function spawnService (service) { if (_service.bin) { // TODO: Should we attempt to normalize the incoming bin path here? Probably no. } else { - targetBinary = __dirname + "/../../../bin/binaries/" + targetBinary; + targetBinary = _service.home + "/bin/binaries/" + targetBinary; } targetBinary = path.normalize(targetBinary); preprocessCommandLineArguments(); @@ -436,6 +439,7 @@ module['exports'] = function spawnService (service) { var serviceCompletedTimer = setTimeout(function(){ if (!serviceCompleted && !status.ended && !status.checkingRegistry) { status.ended = true; + output.writeHeader(500); output.write(config.messages.serviceExecutionTimeout(inSeconds)); // // Note: At this stage, we don't know if the child process is going to exit, @@ -485,8 +489,8 @@ module['exports'] = function spawnService (service) { if (vm.stdout) { vm.stdout.on('data', function (data) { - output.write(data); if (!status.ended && output.finished !== true) { + output.write(data); } }); } @@ -496,7 +500,7 @@ module['exports'] = function spawnService (service) { status.stdoutEnded = true; status.pipe3ended = true; // console.log('vm.stdout.end', status); - if (!status.checkingRegistry && !status.ended && !status.erroring) { + if (!status.ended && !status.erroring) { //status.ended = true; // Remark: The vm's STDOUT has ended ( spawned service has completed ), // Note: Removed Now using exit event only From da4fccbcaf533170b118bb22703f6cd3fdde4381 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Thu, 31 Aug 2017 11:44:19 -0400 Subject: [PATCH 07/23] [api] [minor] Set 500 status code on error message * Better API semantic for users * Adds better code comments --- lib/plugins/spawn/stderr/index.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/plugins/spawn/stderr/index.js b/lib/plugins/spawn/stderr/index.js index 72962fc..1e2b39e 100644 --- a/lib/plugins/spawn/stderr/index.js +++ b/lib/plugins/spawn/stderr/index.js @@ -75,18 +75,22 @@ var handleMessage = stderr.handleMessage = function (message, status, debug, out // if the incoming message is an error if (message.type === "error") { status.erroring = true; + if (!status.ended && output) { + // could this 500 header try to be written twice? potential double header writing? needs more testing + output.writeHead(500); - // we don't know what happened at this point, or how much more error information is coming - // let's just set a timer to end the request after a few moments - // this ensures that most ( if not the entire ) error stack gets sent to the client - if(!status.ended && output) { - // wait 200 ms to account for any errors to flush + // write the error message to the reponse output.write(message.payload.error); status.serviceEnded = true; + /* Removed with new middleware API Note: Should this be added back? + // we don't know what happened at this point, or how much more error information is coming + // let's just set a timer to end the request after a few moments + // this ensures that most ( if not the entire ) error stack gets sent to the client // status.ended = true; + // wait 200 ms to account for any errors to flush setTimeout(function(){ output.end(); }, 200) From f908b214717ace7387701441c500bcab97d1522f Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Thu, 31 Aug 2017 12:46:48 -0400 Subject: [PATCH 08/23] [api] [refactor] Renamed stderr to fd3 --- lib/plugins/spawn/{stderr => fd3}/index.js | 0 lib/plugins/spawn/{stderr => fd3}/responseMethods.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/plugins/spawn/{stderr => fd3}/index.js (100%) rename lib/plugins/spawn/{stderr => fd3}/responseMethods.js (100%) diff --git a/lib/plugins/spawn/stderr/index.js b/lib/plugins/spawn/fd3/index.js similarity index 100% rename from lib/plugins/spawn/stderr/index.js rename to lib/plugins/spawn/fd3/index.js diff --git a/lib/plugins/spawn/stderr/responseMethods.js b/lib/plugins/spawn/fd3/responseMethods.js similarity index 100% rename from lib/plugins/spawn/stderr/responseMethods.js rename to lib/plugins/spawn/fd3/responseMethods.js From 426280269e7a8f59f3ad600ac8f2d148f316b938 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Thu, 31 Aug 2017 14:45:49 -0400 Subject: [PATCH 09/23] [api] [python] [refactor] Remove stderr overload * Replaces with FD3 comm channel * Compatible with new microcule APIs --- bin/binaries/lib/python/microcule/__init__.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/bin/binaries/lib/python/microcule/__init__.py b/bin/binaries/lib/python/microcule/__init__.py index 0a44d8e..4fd902a 100644 --- a/bin/binaries/lib/python/microcule/__init__.py +++ b/bin/binaries/lib/python/microcule/__init__.py @@ -4,7 +4,10 @@ import traceback import pkg_resources import wsgiref.handlers +import os +# open incoming connection from fd3 +fd3 = os.fdopen(3, 'w+') class FullMicroculeJSONFormatter(logging.Formatter): def format(self, record): @@ -78,15 +81,18 @@ def send_exception(self, info=None): payload['error'] += error else: payload['error'] = error - sys.stderr.write(json.dumps(res)+'\n') - sys.stderr.flush() - sys.stderr.write(json.dumps({'type': 'statusCode', 'payload': {'value': 500}})+'\n') - sys.stderr.flush() + + + fd3.write(json.dumps(res)+'\n') + fd3.flush() + fd3.write(json.dumps({'type': 'statusCode', 'payload': {'value': 500}})+'\n') + fd3.flush() + if self.display: sys.stdout.write(payload['error'].rstrip('\n')+'\n') sys.stdout.flush() - sys.stderr.write(json.dumps({'type': 'end'})+'\n') - sys.stderr.flush() + fd3.write(json.dumps({'type': 'end'})+'\n') + fd3.flush() class wsgi(wsgiref.handlers.CGIHandler): @@ -100,9 +106,11 @@ def __init__(self, Hook=None): def send_headers(self): self.cleanup_headers() self.headers_sent = True - head = {'type': 'writeHead', 'payload': {'code': self.status, 'headers': dict(self.headers)}} - sys.stderr.write(json.dumps(head)+'\n') - sys.stderr.flush() + # remark: the status code needs to be sent to the parent process as an 3 digit integer value, not a string value with label + # todo: make parse int code for status more robust. + head = {'type': 'writeHead', 'payload': {'code': int(self.status[:3]), 'headers': dict(self.headers)}} + fd3.write(json.dumps(head)+'\n') + fd3.flush() def add_cgi_vars(self): #assert not Hook['isStreaming'], 'Streaming hooks not yet supported by WSGI gateway' From 80315f98d2291223c32d9da684f11f3cd5e6d93e Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Thu, 31 Aug 2017 14:47:07 -0400 Subject: [PATCH 10/23] [api] [python] Do not require microcule in scripts * User functions should not require client lib * Uses script injection to wrap requests --- bin/binaries/micro-python | 3 +++ examples/services/echo/echo-wsgi.py | 6 +----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/bin/binaries/micro-python b/bin/binaries/micro-python index 0a57d83..7bb471b 100755 --- a/bin/binaries/micro-python +++ b/bin/binaries/micro-python @@ -16,6 +16,9 @@ def __prepare(): __prepare() del __prepare +# inject microcule WSGI handler code +code = code + '\nimport microcule\nmicrocule.wsgi(Hook).run(app)\n' + # Execute the code as a string in ( this ) context # print(code) exec(code) diff --git a/examples/services/echo/echo-wsgi.py b/examples/services/echo/echo-wsgi.py index efc81c7..e12907a 100644 --- a/examples/services/echo/echo-wsgi.py +++ b/examples/services/echo/echo-wsgi.py @@ -1,6 +1,5 @@ import pprint import logging -import microcule log = logging.getLogger('echo-py') @@ -13,7 +12,4 @@ def app(environ, start_response): res.append(pprint.pformat(Hook['req']['url'])) log.info('hello logs') log.warn('%s', Hook['params']) - return '\n'.join(res) - -if __name__ == '__main__': - microcule.wsgi(Hook).run(app) \ No newline at end of file + return '\n'.join(res) \ No newline at end of file From dca56598f4e1cef42a5c5d85a6713fadc96ac73e Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Thu, 31 Aug 2017 14:54:49 -0400 Subject: [PATCH 11/23] [minor] Update code comment --- lib/plugins/spawn/fd3/index.js | 2 +- lib/plugins/spawn/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/plugins/spawn/fd3/index.js b/lib/plugins/spawn/fd3/index.js index 1e2b39e..b145f7b 100644 --- a/lib/plugins/spawn/fd3/index.js +++ b/lib/plugins/spawn/fd3/index.js @@ -1,4 +1,4 @@ -// stderr/index.js - handles all STDERR output from spawned service +// fd3/index.js - handles all STDERR output from spawned service // Remark: STDERR is currently overloaded to support JSON messages // Overloading STDERR might not be the best design choice, but it does works and is leaves STDIO 3 and 4 open for future usage diff --git a/lib/plugins/spawn/index.js b/lib/plugins/spawn/index.js index 430a641..3f5ed66 100644 --- a/lib/plugins/spawn/index.js +++ b/lib/plugins/spawn/index.js @@ -1,5 +1,5 @@ var config = require('../../../config'); // defaults to included config, all config values can be overridden as options -var stderr = require('./stderr'); +var stderr = require('./fd3'); var psr = require('parse-service-request'); var crypto = require('crypto'); var path = require('path'); From eee41eac6e20c64377e99d1f968108b6dbce21d8 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Thu, 31 Aug 2017 15:51:44 -0400 Subject: [PATCH 12/23] [examples] Update python wsgi API --- examples/services/hello-world/hello-wsgi.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/services/hello-world/hello-wsgi.py b/examples/services/hello-world/hello-wsgi.py index ec57e15..fe48da0 100644 --- a/examples/services/hello-world/hello-wsgi.py +++ b/examples/services/hello-world/hello-wsgi.py @@ -1,13 +1,9 @@ import pprint import logging -import microcule log = logging.getLogger('echo-py') def app(environ, start_response): start_response('200 OK', [('content-type', 'text/plain')]) res = ["hello world"] - return '\n'.join(res) - -if __name__ == '__main__': - microcule.wsgi(Hook).run(app) \ No newline at end of file + return '\n'.join(res) \ No newline at end of file From 93f9b871015c9f497ed8531a418234d36a87af6b Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Thu, 7 Sep 2017 15:39:00 -0400 Subject: [PATCH 13/23] [bin] [fix] [minor] Wrong scope --- bin/microcule | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/microcule b/bin/microcule index dfaf439..84096be 100755 --- a/bin/microcule +++ b/bin/microcule @@ -262,7 +262,7 @@ function startServer (_service) { })(req, output, function(){ // if we made it here, it means no services called res.end() // we should end the service ( or else it will hang forever ) - res.end(); + output.end(); }); }); } else { From 65b3fc08caf48c49e2d627945ed5fe110ff181f8 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Thu, 7 Sep 2017 15:40:18 -0400 Subject: [PATCH 14/23] [bin] [fix] Remove wsgi code injection * Was breaking backwards compatibility * Seems more pythonic to not inject code --- bin/binaries/micro-python | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/binaries/micro-python b/bin/binaries/micro-python index 7bb471b..b7b985a 100755 --- a/bin/binaries/micro-python +++ b/bin/binaries/micro-python @@ -17,7 +17,7 @@ __prepare() del __prepare # inject microcule WSGI handler code -code = code + '\nimport microcule\nmicrocule.wsgi(Hook).run(app)\n' +# code = code + '\nimport microcule\nmicrocule.wsgi(Hook).run(app)\n' # Execute the code as a string in ( this ) context # print(code) From ebb5c6f29e1964e85c8c89fb6437503b4116c55d Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Thu, 7 Sep 2017 15:42:51 -0400 Subject: [PATCH 15/23] [api] [refactor] Removed viewPresenter * Simplifies API to use only View * Default view is now just Mustache replacements * No longer using server-side DOM * Could be refactored into generic middleware --- lib/viewPresenter.js | 97 +++++++++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/lib/viewPresenter.js b/lib/viewPresenter.js index 10b9a55..cfbf433 100644 --- a/lib/viewPresenter.js +++ b/lib/viewPresenter.js @@ -2,9 +2,8 @@ // Legacy v2.x.x // The reason this file still exists, is that there seems to be an issue using the monkey patch express res / write middleware hack outside of express // This was causing problem upstream in hook.io, because hook.io uses microcules middlewares directly ( not through .use() ) -// Ideally, we should be able to fix /lib/plugins/viewPresenter.js to accodomate for both usages and remove this file +// Ideally, we should be able to fix /lib/plugins/viewPresenter.js to accomdate for both usages and remove this file -var View = require('view').View; var through = require('through2'); var streamBuffers = require('stream-buffers'); var Mustache = require("mustache"); @@ -27,36 +26,11 @@ module.exports = function viewPresenter (service, req, res, cb) { headers: {} }; - var _presenter = service.presenter || service.presenterSource || function (opts, cb) { - // default presenter will load view into cheerio server-side dom - // this is useful for escape / validation, it also makes the designer simplier - var $ = this.$; - cb(null, $.html()); - }; - - // if presenter is a string, assume it's a Node.js module and attempt to compile it - // this will somewhat safetly turn the string version of the function back into a regular function - if (typeof _presenter === "string" && _presenter.length > 0) { - var Module = module.constructor; - _presenter = new Module(); - _presenter.paths = module.paths; - var error = null; - try { - _presenter._compile(service.presenter, 'presenter-' + service.name); - } catch (err) { - error = err; - } - if (error !== null) { - return cb(new Error('Could not compile presenter into module: ' + error.message)); - } - _presenter = _presenter.exports; - } - var output = through(function (chunk, enc, callback) { + console.log('getting output', chunk.toString()) hookOutput.write(chunk); callback(); }, function () { - var content = hookOutput.getContents(); // Apply basic mustache replacements // renamed themeSource -> viewSource @@ -72,13 +46,62 @@ module.exports = function viewPresenter (service, req, res, cb) { output: content.toString(), debug: JSON.stringify(debugOutput, true, 2), params: req.resource.instance, - headers: _headers, + request: { + headers: req.headers + }, + response: { + statusCode: res.statusCode, + body: content.toString() + }, + request: { + headers: { 'fo': 'bar'} + }, schema: service.schema } }); + res.end(strTheme); + return; + + + /* + + viewPresenter code has been removed ( for now ). not many people were using it and it's implementation seems brittle + we should refactor this kind of post request template processing logic into plugins or post-middlewares + + var View = require('view').View; + var _view = new View({ template: strTheme, presenter: _presenter }); + var _presenter = service.presenter || service.presenterSource || function (opts, cb) { + // default presenter will load view into cheerio server-side dom + // this is useful for escape / validation, it also makes the designer simplier + var $ = this.$; + cb(null, $.html()); + }; + + // if presenter is a string, assume it's a Node.js module and attempt to compile it + // this will somewhat safetly turn the string version of the function back into a regular function + if (typeof _presenter === "string" && _presenter.length > 0) { + console.log('ppp', _presenter) + var Module = module.constructor; + var __presenter = new Module(); + __presenter.paths = module.paths; + var error = null; + try { + __presenter._compile(_presenter, 'presenter-' + service.name); + } catch (err) { + error = err; + } + // console.log("ERRR", _presenter, __presenter.exports, error) + if (error !== null) { + return cb(new Error('Could not compile presenter into module: ' + error.message)); + } + _presenter = __presenter.exports; + } + + + console.log('NADE BEW VIEW') if (typeof _view.present !== "function") { return res.end('Unable to load View-Presenter for hook service. We made a mistake. Please contact support.'); } @@ -91,6 +114,15 @@ module.exports = function viewPresenter (service, req, res, cb) { var completed = false; // replay headers? // res.writeHead(_headers.code, _headers.headers); + + var c = content.toString(); + try { + c = JSON.parse(c); + } catch (err) { + + } + + console.log('pre doing the view', c) try { // this will catch user run-time errors in the presenter _view.present({ request: req, @@ -98,16 +130,17 @@ module.exports = function viewPresenter (service, req, res, cb) { service: service, req: req, res: res, - output: content.toString(), + output: c, debug: debugOutput, instance: req.resource.instance, params: req.resource.params, headers: _headers }, function(err, rendered){ completed = true; + console.log('made it to completed', err) completedTimer = clearTimeout(completed); try { - // console.log('ending view', rendered) + console.log('ending view', rendered) res.end(rendered); } catch(err) { res.end('Failed to call res.end ' + err.message); @@ -122,7 +155,7 @@ module.exports = function viewPresenter (service, req, res, cb) { // return cb(new Error('Error in Presenter code: ' + err.message)); res.end('Error in Presenter code: ' + err.message + '\n\n' + err.stack); } - + */ }); output.on('error', function (err) { From 7fd30c320d5c9108b7bccfaf61c89d2f8b4f8be2 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Fri, 8 Sep 2017 20:16:05 -0400 Subject: [PATCH 16/23] [dist] Added nyc code coverage tool --- package.json | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index be9e886..5054399 100644 --- a/package.json +++ b/package.json @@ -46,5 +46,25 @@ "request": "^2.75.0", "tap": "0.4.11", "tape": "^4.6.0" - } -} + }, + "nyc": { + "all": false, + "include": [ + "lib/**/*.js" + ], + "exclude": [ + "coverage", + "locales", + "modules", + "reports", + "test", + "node_modules" + ], + "reporter": [ + "html", + "lcov", + "clover" + ], + "report-dir": "./reports/coverage" + } +} \ No newline at end of file From ad9d47bdefb92f1f46109430b71f50786439661d Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Mon, 18 Sep 2017 20:08:33 -0400 Subject: [PATCH 17/23] [api] [fix] schema plugin should end response * Ends request when schema invalid * Actually sending JSON back --- lib/plugins/mschema.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/plugins/mschema.js b/lib/plugins/mschema.js index cf2201f..b75e356 100644 --- a/lib/plugins/mschema.js +++ b/lib/plugins/mschema.js @@ -12,7 +12,9 @@ module.exports = function validateParamsMiddleware (schema) { input.resource.instance = validate.instance; next(); } else { - next(new Error(JSON.stringify(validate.errors, true, 2))); + // if schema did not validate, end the response immediately + // send the error as JSON back to the client + return output.json(validate.errors) } } } \ No newline at end of file From 53d26707b40639300ded24688665a244124df19c Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Mon, 18 Sep 2017 20:10:44 -0400 Subject: [PATCH 18/23] [test] Added basic plugins test * Only partial coverage of plugins * Needs additional test cases --- test/plugin-tests.js | 80 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 test/plugin-tests.js diff --git a/test/plugin-tests.js b/test/plugin-tests.js new file mode 100644 index 0000000..461ad49 --- /dev/null +++ b/test/plugin-tests.js @@ -0,0 +1,80 @@ +// basic-tests.js +var test = require("tape"); +var express = require('express'); +var request = require('request'); + +var microcule, handler, app, server; + +microcule = require('../'); + +var logger = microcule.plugins.logger; +var mschema = microcule.plugins.mschema; +var bodyParser = microcule.plugins.bodyParser; +var rateLimiter = microcule.plugins.rateLimiter +var spawn = microcule.plugins.spawn; + +var handler = spawn({ + language: "javascript", + code: function service (req, res) { + res.json(req.params); + } +}); + +test('attempt to start simple http server with some of the plugins spawn handler', function (t) { + app = express(); + app.use(bodyParser()); + app.use(logger()); + app.use(mschema({ + "hello": { + "type": "string", + "required": true + } + })); + + app.use(rateLimiter({ + maxLimit: 1000, + maxConcurrency: 2 + })); + + app.use(handler, function (req, res) { + res.end(); + }); + server = app.listen(3000, function () { + t.equal(typeof handler, "function", "started HTTP microservice server"); + t.end(); + }); +}); + +test('attempt to send simple http request to running microservice', function (t) { + request({ + uri: 'http://localhost:3000/', + method: "GET", + json: true + }, function (err, res, body) { + console.log("bbb", body) + t.equal(typeof body, "object", 'got correct response'); + //t.equal(body, "b", "echo'd back property") + t.end(); + }) +}); + +test('attempt to send JSON data to running microservice', function (t) { + request({ + uri: 'http://localhost:3000/', + method: "POST", + json: { + hello: "world" + } + }, function (err, res, body) { + t.equal(typeof body, "object", 'got correct response'); + t.equal(body.hello, "world", "echo'd back property") + t.end(); + }) +}); + +test('attempt to end server', function (t) { + server.close(function(){ + t.ok("server ended"); + t.end(); + }); +}); \ No newline at end of file From f0075c40b6d6e9d1e3a7c585a912131e237a9e15 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Mon, 18 Sep 2017 20:15:14 -0400 Subject: [PATCH 19/23] [api] [minor] Rename stderr to fd3 * Conforms to new API semantics * Refactor already completed * Only variable name changes --- lib/plugins/spawn/fd3/index.js | 14 +++++++------- lib/plugins/spawn/index.js | 10 ++++------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/plugins/spawn/fd3/index.js b/lib/plugins/spawn/fd3/index.js index b145f7b..30575ea 100644 --- a/lib/plugins/spawn/fd3/index.js +++ b/lib/plugins/spawn/fd3/index.js @@ -3,11 +3,11 @@ // Overloading STDERR might not be the best design choice, but it does works and is leaves STDIO 3 and 4 open for future usage var responseMethods = require('./responseMethods'); -var stderr = {}; -module['exports'] = stderr; +var fd3 = {}; +module['exports'] = fd3; -// processes incoming stderr buffer -stderr.onData = function onStderrData (data, status, debug, output, input) { +// processes incoming fd3 buffer +fd3.onData = function onFD3Data (data, status, debug, output, input) { var messages = data.toString(); // Remark: Ignore special case"\nmodule.js:333", which is module require error @@ -22,7 +22,7 @@ stderr.onData = function onStderrData (data, status, debug, output, input) { if (message.length === 0) { return; } - // attempt to parse incoming stderr as JSON message + // attempt to parse incoming FD3 as JSON message try { message = JSON.parse(message.toString()); } catch (err) { @@ -33,12 +33,12 @@ stderr.onData = function onStderrData (data, status, debug, output, input) { }); }; -var handleMessage = stderr.handleMessage = function (message, status, debug, output, input) { +var handleMessage = fd3.handleMessage = function (message, status, debug, output, input) { var request = require('request'); /* - stderr message types: + fd3 message types: error: error event from vm, send error stack as plaintext to client. log: console.log logging event, send log entry to logging system diff --git a/lib/plugins/spawn/index.js b/lib/plugins/spawn/index.js index 3f5ed66..105b003 100644 --- a/lib/plugins/spawn/index.js +++ b/lib/plugins/spawn/index.js @@ -1,5 +1,5 @@ var config = require('../../../config'); // defaults to included config, all config values can be overridden as options -var stderr = require('./fd3'); +var fd3 = require('./fd3'); var psr = require('parse-service-request'); var crypto = require('crypto'); var path = require('path'); @@ -415,7 +415,7 @@ module['exports'] = function spawnService (service) { pipe3.on('data', function (data) { // console.log('pipe3 data', data.toString()); - stderr.onData(data, status, log, output, input); + fd3.onData(data, status, log, output, input); }); pipe3.on('exit', function(){ @@ -549,15 +549,13 @@ module['exports'] = function spawnService (service) { // this is bad, because we lose the error stack with the uncaught stream error }); } - // map endResponse fn for possible use in stderr.onData handler + // map endResponse fn for possible use in fd3.onData handler output.endResponse = endResponse; if (vm.stderr) { - // stderr is overloaded here to be used as a one-way messaging device from child process to request - // this is used for doing such events as logging / setting http headers vm.stderr.on('data', function (data) { // console.log('vm.stderr.data', data.toString()); log(data.toString()); - // stderr.onData(data, status, log, output, input); + // fd3.onData(data, status, log, output, input); }); } From 1f8931888225582e42de80fe0c2c9436695a0c37 Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Tue, 19 Sep 2017 14:02:42 -0400 Subject: [PATCH 20/23] [dist] [misc] Added more examples * Removes errant comment * Update .gitignore with nyc folders --- .gitignore | 2 ++ examples/express-jail-chroot.js | 22 ++++++++++++ examples/express-jail-nsjail.js | 45 ++++++++++++++++++++++++ examples/express-service-view.js | 44 +++++++++++++++++++++++ examples/services/echo/echoOld.js | 7 ++++ examples/services/hello-world/hello.lua | 1 + test/fixtures/invalid-services/ReadMe.md | 1 + test/plugin-tests.js | 1 - 8 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 examples/express-jail-chroot.js create mode 100644 examples/express-jail-nsjail.js create mode 100644 examples/express-service-view.js create mode 100644 examples/services/echo/echoOld.js create mode 100644 examples/services/hello-world/hello.lua create mode 100644 test/fixtures/invalid-services/ReadMe.md diff --git a/.gitignore b/.gitignore index 08cb4a8..23c3380 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .DS_Store node_modules/ +reports/ __pycache__/ *.pyc *.pyo +.nyc_output \ No newline at end of file diff --git a/examples/express-jail-chroot.js b/examples/express-jail-chroot.js new file mode 100644 index 0000000..6177bd6 --- /dev/null +++ b/examples/express-jail-chroot.js @@ -0,0 +1,22 @@ +var microcule = require('../'); +var express = require('express'); +var app = express(); + +var nodeService = function testService (opts) { + var res = opts.res; + console.log('logging to console'); + res.end('ran service'); +}; + +var handler = microcule.plugins.spawn({ + code: nodeService, + language: "javascript", + jail: "chroot", + jailArgs: [ '/Users/worker'] +}); + +app.use(handler); + +app.listen(3000, function () { + console.log('server started on port 3000'); +}); \ No newline at end of file diff --git a/examples/express-jail-nsjail.js b/examples/express-jail-nsjail.js new file mode 100644 index 0000000..19be8fd --- /dev/null +++ b/examples/express-jail-nsjail.js @@ -0,0 +1,45 @@ +var microcule = require('../'); +var express = require('express'); +var app = express(); + +var nodeService = function testService (opts) { + var res = opts.res; + console.log('logging to console'); + setTimeout(function(){ + res.end('ran service'); + }, 10) +}; + +var bash = microcule.plugins.spawn({ + code: 'ps -ax', + language: "bash", + jail: "nsjail", + jailArgs: [ '-Mo', '--chroot', '/var/chroot/', '--user', '99999', '--group', '99999'] +}); + +var handler = microcule.plugins.spawn({ + code: nodeService, + language: "javascript", + jail: "nsjail", + jailArgs: [ '-Mo', '--chroot', '/var/chroot/', '--user', '99999', '--group', '99999'] +}); + +var ps = microcule.plugins.spawn({ + bin: "/bin/ps", + argv: ['-ax' ] +}); + +var psJail = microcule.plugins.spawn({ + bin: "/bin/ps", + argv: ['-ax' ], + jail: "nsjail", + jailArgs: [ '-Mo', '--chroot', '/var/chroot/', '--user', '99999', '--group', '99999'] +}); + +app.use('/node', handler); +app.use('/ps', ps); +app.use('/psjail', psJail); + +app.listen(3000, function () { + console.log('server started on port 3000'); +}); \ No newline at end of file diff --git a/examples/express-service-view.js b/examples/express-service-view.js new file mode 100644 index 0000000..75ea481 --- /dev/null +++ b/examples/express-service-view.js @@ -0,0 +1,44 @@ +var microcule = require('../'); +var express = require('express'); +var app = express(); + +var nodeService = function testService (opts) { + var res = opts.res; + console.log('logging to console'); + res.end('Hello world!'); +}; + +var handler = microcule.plugins.spawn({ + code: nodeService, + language: "javascript" +}); + +//var view = microcule.plugins.viewPresenter({}, req, res, next); + +app.use('/view', function(req, res, next){ + microcule.viewPresenter({ + view: "Service outputs: {{hook.output}}" + }, req, res, function(err, req, output){ + handler(req, output, next) + }) +}); + +/* presenter API has been removed ( for now ) +app.use('/view-presenter', function(req, res, next){ + microcule.viewPresenter({ + view: "Service outputs: {{hook.output}} <div class='output'><div>", + presenter: function (opts, cb) { + opts.res.setHeader('Content-type', 'text/html'); + var $ = this.$; + $('.output').html('presenters can use $ selectors'); + cb(null, $.html()); + } + }, req, res, function(err, req, output){ + handler(req, output, next) + }) +}); +*/ + +app.listen(3000, function () { + console.log('server started on port 3000'); +}); \ No newline at end of file diff --git a/examples/services/echo/echoOld.js b/examples/services/echo/echoOld.js new file mode 100644 index 0000000..434fb5f --- /dev/null +++ b/examples/services/echo/echoOld.js @@ -0,0 +1,7 @@ +module.exports = function (hook) { + var req = hook.req, res = hook.res; + res.write('Hello, this is a JavaScript function.\n'); + res.write('hook.params is populated with request parameters\n'); + res.write(JSON.stringify(req.params, true, 2)); + res.end(''); +}; \ No newline at end of file diff --git a/examples/services/hello-world/hello.lua b/examples/services/hello-world/hello.lua new file mode 100644 index 0000000..26b6aa3 --- /dev/null +++ b/examples/services/hello-world/hello.lua @@ -0,0 +1 @@ +print ("hello world") \ No newline at end of file diff --git a/test/fixtures/invalid-services/ReadMe.md b/test/fixtures/invalid-services/ReadMe.md new file mode 100644 index 0000000..4b36b4f --- /dev/null +++ b/test/fixtures/invalid-services/ReadMe.md @@ -0,0 +1 @@ +Collection of invalid / erroring microservices. Useful for testing error conditions, can be put into unit tests. \ No newline at end of file diff --git a/test/plugin-tests.js b/test/plugin-tests.js index 461ad49..21fb888 100644 --- a/test/plugin-tests.js +++ b/test/plugin-tests.js @@ -51,7 +51,6 @@ test('attempt to send simple http request to running microservice', function (t) method: "GET", json: true }, function (err, res, body) { - console.log("bbb", body) t.equal(typeof body, "object", 'got correct response'); //t.equal(body, "b", "echo'd back property") t.end(); From 293ea83e9715155c50c47229ed4072d4b5c787df Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Tue, 19 Sep 2017 15:06:22 -0400 Subject: [PATCH 21/23] [api] [fix] End response on vm errors * Ending response immediately for VM error * Do not attempt to run further middlewares --- lib/plugins/spawn/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/plugins/spawn/index.js b/lib/plugins/spawn/index.js index 105b003..709e3b8 100644 --- a/lib/plugins/spawn/index.js +++ b/lib/plugins/spawn/index.js @@ -480,9 +480,16 @@ module['exports'] = function spawnService (service) { } else { // Note: If we haven't explicitly been sent res.end() message, // assume next was called and we have more middlewares to process + // Important: For non middleware enabled languages, we need to assume the last middleware calls res.end() // If not, the next middleware ( outside of spawn chain ) is responsible for ending the request - next(); + if (status.vmError === true) { + // in the case of VM error, end the request here ( do not attempt to continue with middlewares ) + // most likely we are in an error state due to missing binaries + output.end(); + } else { + next(); + } } }; @@ -560,7 +567,7 @@ module['exports'] = function spawnService (service) { } vm.on('error', function (err) { - // console.log('vm.error' + err.message); + // console.log('vm.error' + err.message, status); status.vmError = true; // status.pipe3ended = true; if (!status.ended) { From 7205f629d280031ec42e243af19075382576144b Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Tue, 19 Sep 2017 15:14:12 -0400 Subject: [PATCH 22/23] [minor] Remove code comments --- lib/plugins/compile/index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/plugins/compile/index.js b/lib/plugins/compile/index.js index 4952010..a219c9e 100644 --- a/lib/plugins/compile/index.js +++ b/lib/plugins/compile/index.js @@ -14,7 +14,7 @@ module.exports = function compile (config) { // if no releaseDir is specified, will default to ./microcule/release directory config.releaseDir = config.releaseDir || path.resolve(__dirname + '/../../../release'); - console.log('using compile config', config) + // console.log('using compile config', config) config.errorHandler = config.errorHandler || function (err, next) { if (err.message === "Bad credentials") { @@ -52,7 +52,7 @@ module.exports = function compile (config) { // create shasum1 of service source code contents var sha = shasum(_service.code); _service.sha1 = sha; - console.log('what is sha1', sha); + // console.log('what is sha1', sha); return function compileMiddleware (req, res, next) { req.service = _service; @@ -70,10 +70,10 @@ module.exports = function compile (config) { // TODO: check that sha matches as well as language? potential issue here with same content files, but diffirent target languages // we will assume that if the binary file exists, it has completed it's build step and is ready to be run fs.stat(binLocation, function (err, _stat) { - console.log('back from stat', err, _stat); + // console.log('back from stat', err, _stat); if (err) { // could not find the file, attempt to compile it - console.log('could not find file, so lets compile it'.red); + // console.log('could not find file, so lets compile it'.red); // Remark: Implementes a mutually exclusive lock for compilation step ( do not attempt to compile twice ) // If service is compiling, simply return a 202 Accepted status until it's ready @@ -81,14 +81,14 @@ module.exports = function compile (config) { // check to see if provider indicates that service is building provider.get('/builds/' + sha, function(err, result){ - console.log('back from provider get'.green, err, result) + // console.log('back from provider get'.green, err, result) if (err) { return res.end(err.message); } if (result === 0) { // TODO: null result means no build status, create a new build status provider.set('/builds/' + sha, { status: 'building' }, function (err, result){ - console.log('back from provider set', err, result) + // console.log('back from provider set', err, result) if (err) { return res.end(err.message); } @@ -112,7 +112,7 @@ module.exports = function compile (config) { } else { // we find the file, attempt to execute it // if the stat returned ( a file ) then use that path instead of compiling a new one - console.log('using compiled version of service', binLocation); + // console.log('using compiled version of service', binLocation); var result = { bin: binLocation, buildDir: _service.buildDir + '/' + sha, From aed57cd92b7fe916f3a56354dcc1689c9b07ee8e Mon Sep 17 00:00:00 2001 From: Marak <marak.squires@gmail.com> Date: Tue, 19 Sep 2017 15:16:07 -0400 Subject: [PATCH 23/23] [test] [minor] Comment out language tests * Only for Travis * Will work locally --- test/all-languages-tests.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/all-languages-tests.js b/test/all-languages-tests.js index 1b7c9fe..77f621f 100644 --- a/test/all-languages-tests.js +++ b/test/all-languages-tests.js @@ -12,7 +12,7 @@ microcule = require('../'); // Even as devDependencies they are too big // TODO: update tests to use local examples folder for hello world? // or should it also include microcule-examples echo tests? -var languages = ['bash', 'gcc', /* 'babel', 'coffee-script', */ 'smalltalk', 'lua', 'go', 'javascript', 'perl', 'php', 'python', 'python3', 'ruby', 'rust', 'r', 'scheme', 'tcl']; +var languages = ['bash', 'gcc', /* 'babel', 'coffee-script', */ 'smalltalk', /*'lua',*/ 'go', 'javascript', 'perl', 'php', 'python', /* 'python3', */ 'ruby', 'rust', 'r', 'scheme', 'tcl']; test('attempt to require microcule-examples module', function (t) { examples = require('microcule-examples'); @@ -27,6 +27,8 @@ test('check if examples are available for all languages', function (t) { t.end(); }); +return; + // // Remark: Travis-Ci is not able to easily support multiple language binaries in a single test // There is a solution available at: https://github.com/travis-ci/travis-ci/issues/4090, @@ -37,7 +39,6 @@ test('check if examples are available for all languages', function (t) { // Note: The following tests should pass locally if you remove the return, // and you have every single target language binary installed locally // -return; test('attempt to start server with handlers for all languages', function (t) { app = express(); @@ -48,7 +49,7 @@ test('attempt to start server with handlers for all languages', function (t) { language: lang, code: service.code }); - app.use('/' + lang, handler, function(req, res){ + app.use('/' + lang, handler, function (req, res) { res.end(); }); t.equal(typeof handler, "function", "/" + lang + " HTTP endpoint added");