Permalink
Browse files

fist cut

  • Loading branch information...
0 parents commit 81ef90d454c770428c9af70eb462cef8ab6df9ff @tjanczuk committed Feb 10, 2012
Showing with 388 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +13 −0 LICENSE.txt
  3. +24 −0 package.json
  4. +15 −0 src/cert.pem
  5. +11 −0 src/csr.pem
  6. +75 −0 src/haiku-http.js
  7. +15 −0 src/key.pem
  8. +37 −0 src/master.js
  9. +196 −0 src/worker.js
@@ -0,0 +1,2 @@
+node_modules
+.DS_Store
@@ -0,0 +1,13 @@
+ Copyright 2012 Tomasz Janczuk
+
+ Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
@@ -0,0 +1,24 @@
+{
+ "name": "haiku-http",
+ "description": "Runtime for simple HTTP web APIs",
+ "version": "0.1.0",
+ "author": {
+ "name": "Tomasz Janczuk <tomasz@janczuk.org>",
+ "url": "http://tomasz.janczuk.org"
+ },
+ "dependencies": {
+ "optimist": "0.3.1"
+ },
+ "devDependencies": {
+ },
+ "homepage": "http://github.com/tjanczuk/haiku-http",
+ "bugs": {
+ "url": "http://github.com/tjanczuk/haiku-http/issues"
+ },
+ "keywords": ["http", "application", "web", "api"],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/tjanczuk/haiku-http.git"
+ },
+ "engines": { "node": ">= 0.7.0-pre" }
+}
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICRTCCAa4CCQCywR0r8/wSgjANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJV
+UzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1JlZG1vbmQxITAfBgNVBAoTGEludGVy
+bmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAxQNKi5qYW5jenVrLm9yZzAeFw0x
+MjAyMDIxOTE5MjNaFw0xMjAzMDMxOTE5MjNaMGcxCzAJBgNVBAYTAlVTMQswCQYD
+VQQIEwJXQTEQMA4GA1UEBxMHUmVkbW9uZDEhMB8GA1UEChMYSW50ZXJuZXQgV2lk
+Z2l0cyBQdHkgTHRkMRYwFAYDVQQDFA0qLmphbmN6dWsub3JnMIGfMA0GCSqGSIb3
+DQEBAQUAA4GNADCBiQKBgQC6KNZ5b9Sb+n1zZ0ImhDEZST45m64tM+nLjDd6jfA1
+OBEdJo9hrYcqeEaiP3NneYGRUwWWtvcAuhXuajG0Df8RzGDJDUa7WlZIEZkXTUr9
+9Ykixj8F85sQHk5sVsTfjCEw9bbqdS/uUMzjFcGGf+9r7qe1E2xOiDAwd5ZNMyr4
+yQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAKfimB87gNf/Jzn7KZ8B+lG+IZbTQq8Q
+fCXZph1oUx2mmYjXQwAn8gtCuu5TbBXng9UMaeFBD9UGX50MyTZf+jzgwdKCRH66
+m9CHYZmAsLr3zfoYNNoyiLfOaUM6FA2YNjfsGkLQk/yj4STOtv6SihtB8YghC6b/
+fkPRx5Zpub7I
+-----END CERTIFICATE-----
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBpzCCARACAQAwZzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQH
+EwdSZWRtb25kMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFjAU
+BgNVBAMUDSouamFuY3p1ay5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
+ALoo1nlv1Jv6fXNnQiaEMRlJPjmbri0z6cuMN3qN8DU4ER0mj2Gthyp4RqI/c2d5
+gZFTBZa29wC6Fe5qMbQN/xHMYMkNRrtaVkgRmRdNSv31iSLGPwXzmxAeTmxWxN+M
+ITD1tup1L+5QzOMVwYZ/72vup7UTbE6IMDB3lk0zKvjJAgMBAAGgADANBgkqhkiG
+9w0BAQUFAAOBgQAR7YaihbiEgrte1y0sKEvl8XCjyS2QNpwamgwV4nsWSarx3HH+
+olrPe9kin/Teph0bWNbKeWdSfVj9SUdDABZ7GqRkaarfeh0RZpuUgYL7MutdeVK1
+Trx75v3F5yh08gaL3/qPoAvF64ceVqMe73f0x9HGUAHtV0EQ6/wA08+5LA==
+-----END CERTIFICATE REQUEST-----
@@ -0,0 +1,75 @@
+var cluster = require('cluster')
+ , fs = require('fs')
+
+var argv = require('optimist')
+ .usage('Usage: $0')
+ .options('w', {
+ alias: 'workers',
+ description: 'Number of worker processes',
+ default: require('os').cpus().length * 4
+ })
+ .options('p', {
+ alias: 'port',
+ description: 'HTTP listen port',
+ default: 80
+ })
+ .options('s', {
+ alias: 'sslport',
+ description: 'HTTPS listen port',
+ default: 443
+ })
+ .options('c', {
+ alias: 'cert',
+ description: 'Server certificate for SSL',
+ default: './cert.pem'
+ })
+ .options('k', {
+ alias: 'key',
+ description: 'Private key for SSL',
+ default: './key.pem'
+ })
+ .options('x', {
+ alias: 'proxy',
+ description: 'HTTP proxy in host:port format for outgoing requests',
+ default: ''
+ })
+ .options('i', {
+ alias: 'maxsize',
+ description: 'Maximum size of a handler in bytes',
+ default: '16384'
+ })
+ .options('t', {
+ alias: 'maxtime',
+ description: 'Maximum clock time in milliseconds for handler execution',
+ default: '5000'
+ })
+ .options('r', {
+ alias: 'maxrequests',
+ description: 'Number of requests before process recycle. Zero for no recycling.',
+ default: '1'
+ })
+ .check(function (args) { return !args.help; })
+ .check(function (args) { return args.p != args.s; })
+ .check(function (args) {
+ cert = fs.readFileSync(args.c);
+ key = fs.readFileSync(args.k);
+ return true;
+ })
+ .check(function (args) {
+ var proxy = args.x === '' ? process.env.HTTP_PROXY : args.x;
+ if (proxy) {
+ var i = proxy.indexOf(':');
+ args.proxyHost = i == -1 ? proxy : proxy.substring(0, i),
+ args.proxyPort = i == -1 ? 80 : proxy.substring(i + 1)
+ }
+ return true;
+ })
+ .check(function (args) {
+ return NaN !== (args.mr = parseInt(args.mr));
+ })
+ .argv;
+
+if (cluster.isMaster)
+ require('./master.js').main(argv);
+else
+ require('./worker.js').main(argv);
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQC6KNZ5b9Sb+n1zZ0ImhDEZST45m64tM+nLjDd6jfA1OBEdJo9h
+rYcqeEaiP3NneYGRUwWWtvcAuhXuajG0Df8RzGDJDUa7WlZIEZkXTUr99Ykixj8F
+85sQHk5sVsTfjCEw9bbqdS/uUMzjFcGGf+9r7qe1E2xOiDAwd5ZNMyr4yQIDAQAB
+AoGAeVJaDIR0RD8oeQhnlSB7uyX/tp2eEvmNOcmk8msEjDqA9MWHljn4KBaAugau
+GFaYuXQo5UNSkJe16U4uHFEu1HWTQ4OkVjhJ+CacyaZcJV54KT6N4hC8+TiN1Y/b
+4y1TWxHvFByY6+MI6/ZcqdkyzvqTsl0EM3+7CBeSv5Ia2U0CQQDyFzcsxaXRi+9z
+s8jmIiswIYAGcswCJe5u1a3sgACW5AkHRil5FG3s3PDtFcpTLJlDqILYIIRiT4+D
+26vOpT4/AkEAxNr10VQylRgwzXsMDpjCNm6k6J7eJRToZljpAjn30aBvODeGaNu0
+98+PxPd4mhVfw9lXBGI7tNTsh6gBxf2W9wJBANrFT/8NvYNXydPtLCeLySt9mow5
+QVLPpGBUiQ+nvOCeweno5aGdbJkYMECP6H6xVu9lYJifCgMtkqu938ymV1ECQFs6
+rllIj/iQsW1I7RmGqdrYBAzaM1E0E0/7PGEPxE2d8G05Lk1CJOgDhTlfBsFBzpPR
+EYayj8EKPGPR9KBxGZkCQAEA1hwpb4uk6EIv67SiSN09yy5qISJMRH9gyc6AlZmw
+U7ryfkIU8oiJGiYh6C63NoUS8i0ZTZaahyxNxlX9v5I=
+-----END RSA PRIVATE KEY-----
@@ -0,0 +1,37 @@
+var cluster = require('cluster');
+var argv;
+
+function log(thing) {
+ console.log(process.pid + ': ' + thing);
+}
+
+function challange(worker) {
+
+}
+
+function createOneWorker() {
+ var worker = cluster.fork();
+ challange(worker);
+}
+
+exports.main = function (args) {
+ argv = args;
+ log('haiku-http: a runtime for simple HTTP web APIs');
+ log('Number of workers: ' + argv.w);
+ log('HTTP port: ' + argv.p);
+ log('HTTPS port: ' + argv.s);
+ log('HTTP proxy: ' + (argv.proxyHost ? (argv.proxyHost + ':' + argv.proxyPort) : 'none'));
+ log('Max handler size [bytes]: ' + argv.i);
+ log('Max handler execution time [ms]: ' + argv.t);
+ log('Max requests before recycle: ' + argv.r);
+
+ for (var i = 0; i < argv.w; i++)
+ createOneWorker();
+
+ cluster.on('death', function (worker) {
+ log('Worker ' + worker.process.pid + ' exited, creating replacement');
+ createOneWorker();
+ });
+
+ log('haiku-http started. Ctrl-C to terminate.');
+}
@@ -0,0 +1,196 @@
+var http = require('http')
+ , https = require('https')
+ , url = require('url')
+ , vm = require('vm')
+ , cluster = require('cluster')
+
+var cooldown = false
+ , activeRequests = 0
+ , requestCount = 0
+ , argv
+
+function log(thing) {
+ console.log(process.pid + ': ' + thing);
+}
+
+function onRequestFinished(context) {
+ if (!context.finished) {
+ context.finished = true;
+ activeRequests--;
+ if (cooldown && 0 === activeRequests) {
+ process.nextTick(function() {
+ log('Recycling the worker')
+ process.exit();
+ });
+ }
+ }
+}
+
+function haikuError(context, status, error) {
+ log(new Date() + ' Status: ' + status + ', Error: ' + error);
+ try {
+ context.req.resume();
+ context.res.writeHead(status);
+ if ('HEAD' !== context.req.method)
+ context.res.end((typeof error === 'string' ? error : JSON.stringify(error)) + '\n');
+ else
+ context.res.end();
+ }
+ catch (e) {
+ // empty
+ }
+ onRequestFinished(context);
+}
+
+function intercept(instance, func, inspector) {
+ var oldFunc = instance[func];
+ instance[func] = function () {
+ var result = oldFunc.apply(instance, arguments);
+ inspector(arguments, result);
+ return result;
+ }
+}
+
+function createSandbox(context) {
+
+ // limit execution time of the handler to the preconfigured value
+
+ context.timeout = setTimeout(function () {
+ delete context.timeout;
+ haikuError(context, 500, 'Handler ' + context.handlerName + ' did not complete within the time limit of ' + argv.t + 'ms');
+ onRequestFinished(context);
+ }, argv.t);
+
+ // re-enable the server to accept subsequent connection when the response is sent
+
+ intercept(context.res, 'end', function () {
+ if (context.timeout) {
+ clearTimeout(context.timeout);
+ delete context.timeout;
+ onRequestFinished(context);
+ }
+ });
+
+ return {
+ req: context.req,
+ res: context.res,
+ setTimeout: setTimeout,
+ console: console
+ };
+}
+
+function executeHandler(context) {
+ log(new Date() + ' executing ' + context.handlerName);
+
+ context.req.resume();
+ try {
+ vm.runInNewContext(context.handler, createSandbox(context), context.handlerName);
+ }
+ catch (e) {
+ haikuError(context, 500, 'Handler ' + context.handlerName + ' generated an exception at runtime: ' + e);
+ }
+}
+
+function resolveHandler(context) {
+ if (!context.handlerName)
+ return haikuError(context, 400,
+ 'The x-haiku-handler HTTP request header or query paramater must specify the URL of the scriptlet to run.');
+
+ try {
+ context.handlerUrl = url.parse(context.handlerName);
+ }
+ catch (e) {
+ return haikuError(context, 400, 'The x-haiku-handler parameter must be a valid URL that resolves to a JavaScript scriptlet.');
+ }
+
+ var engine;
+ if (context.handlerUrl.protocol === 'http:') {
+ engine = http;
+ context.handlerUrl.port = context.handlerUrl.port || 80;
+ }
+ else if (context.handlerUrl.protocol === 'https:') {
+ engine = https;
+ context.handlerUrl.port = context.handlerUrl.port || 443;
+ }
+ else
+ return haikuError(context, 400, 'The x-haiku-handler parameter specifies unsupported protocol. Only http and https are supported.');
+
+ var handlerRequest;
+ var processResponse = function(res) {
+ context.handler = '';
+ var length = 0;
+ res.on('data', function(chunk) {
+ length += chunk.length;
+ if (length > argv.i) {
+ handlerRequest.abort();
+ return haikuError(context, 400, 'The size of the handler exceeded the quota of ' + argv.i + ' bytes.');
+ }
+ context.handler += chunk;
+ })
+ .on('end', function() {
+ if (res.statusCode === 200)
+ executeHandler(context);
+ else if (res.statusCode === 302 && context.redirect < 3) {
+ context.handlerName = res.headers['location'];
+ context.redirect++;
+ resolveHandler(context);
+ }
+ else
+ return haikuError(context, 400, 'HTTP error when obtaining handler code from ' + context.handlerName + ': ' + res.statusCode);
+ });
+ }
+
+ var processError = function(error) {
+ haikuError(context, 400, 'Unable to obtain HTTP handler code from ' + context.handlerName + ': ' + error);
+ }
+
+ if (argv.proxyHost) {
+ // HTTPS or HTTP request through HTTP proxy
+ http.request({ // establishing a tunnel
+ host: argv.proxyHost,
+ port: argv.proxyPort,
+ method: 'CONNECT',
+ path: context.handlerUrl.hostname + ':' + context.handlerUrl.port
+ }).on('connect', function(pres, socket, head) {
+ if (pres.statusCode !== 200)
+ return haikuError(context, 400, 'Unable to connect to the host ' + context.host);
+ else
+ handlerRequest = engine.get({
+ host: context.handlerUrl.host,
+ path: context.handlerUrl.path,
+ socket: socket, // using a tunnel
+ agent: false // cannot use a default agent
+ }, processResponse).on('error', processError);
+ }).on('error', processError).end();
+ }
+ else // no proxy
+ handlerRequest = engine.get({
+ host: context.handlerUrl.host,
+ path: context.handlerUrl.path
+ }, processResponse).on('error', processError);
+}
+
+function processRequest(req, res) {
+ activeRequests++;
+
+ if (!cooldown && argv.r > 0 && ++requestCount >= argv.r) {
+ log('Entering cooldown mode with active requests: ' + activeRequests);
+ cooldown = true;
+ httpServer.close();
+ httpsServer.close();
+ }
+
+ req.pause();
+ resolveHandler({
+ req: req,
+ res: res,
+ redirect: 0,
+ handlerName: req.headers['x-haiku-handler'] || url.parse(req.url, true).query['x-haiku-handler']
+ });
+}
+
+exports.main = function(args) {
+ argv = args;
+ httpServer = http.createServer(processRequest).listen(argv.p);
+ httpsServer = https.createServer({ cert: argv.cert, key: argv.key }, processRequest).listen(argv.s);
+}

0 comments on commit 81ef90d

Please sign in to comment.