├── .gitignore ├── LICENSE.txt ├── README.md ├── package.json ├── samples ├── haikus │ ├── console.js │ ├── delayed-hello.js │ ├── hello.js │ ├── https.js │ ├── mongo.js │ ├── proxied-https.js │ ├── request.js │ └── tests │ │ ├── entrypoint_EventEmitter.js │ │ ├── entrypoint_nextTick.js │ │ ├── entrypoint_setInterval.js │ │ └── entrypoint_setTimeout.js └── server.js ├── src ├── cert.pem ├── csr.pem ├── haiku-http.js ├── haikuConsole.js ├── haiku_extensions.cc ├── key.pem ├── master.js ├── sandbox.js └── worker.js ├── test ├── 000_prerequisite.js ├── 200_samples_hello.js ├── 201_samples_delayed-hello.js ├── 202_samples_mongo.js ├── 203_samples_https.js ├── 204_samples_request.js ├── 205_samples_console.js └── 400_entrypoint.js └── wscript /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .lock-wscript 4 | build 5 | src/test* -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2012 Tomasz Janczuk 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-tenant runtime for simple HTTP web APIs 2 | 3 | Haiku-http provides you with a multi-tenant runtime for hosting simple HTTP web APIs implemented in JavaScript: 4 | 5 | - **Sub process multi-tenancy**. You can execute code from multiple tenants within a single process while preserving data integrity of each tenant. You also get a degree of local denial of service prevention: the section about the sandbox below explains features and limitations. 6 | - **Programming model based on node.js**. Programming model for authoring HTTP web APIs is based on a subset of node.js. Implementing an HTTP web API resembles coding the body of the node.js HTTP request handler function. You can currently use HTTP[S], TCP, TLS, and MongoDB. 7 | - **Easy deployment**. You can host the code for HTTP web APIs wherever it can be accessed with an HTTP call from within the runtime. GitHub or Gist work fine. 8 | 9 | ## History, motivation, and goals 10 | 11 | Read more about these aspects at [http://tomasz.janczuk.org](http://tomasz.janczuk.org). 12 | 13 | ## Prerequisites 14 | 15 | - Windows, MacOS, or *nix (tested on Windows 7 & 2008 Server, MacOS Lion, Ubuntu 11.10) 16 | - [node.js v0.7.0 or greater](http://nodejs.org/dist/) 17 | 18 | ## Getting started 19 | 20 | Install haiku-http: 21 | 22 | ``` 23 | npm install haiku-http 24 | ``` 25 | 26 | Start the haiku-http runtime (default settings require ports 80 and 443 to be available): 27 | 28 | ``` 29 | sudo node node_modules/haiku-http/src/haiku-http.js 30 | ``` 31 | 32 | (in an environment with an HTTP proxy, one must specify the proxy address using the `-x` parameter, e.g. `sudo node node_modules/haiku-http/src/haiku-http.js -x itgproxy:80`) 33 | 34 | Open a browser and navigate to 35 | 36 | ``` 37 | http://localhost?x-haiku-handler=https://github.com/tjanczuk/haiku-http/blob/master/samples/haikus/hello.js 38 | ``` 39 | 40 | You should see a 'Hello, world!' message, which is the result of executing the haiku-http web API implemented at https://github.com/tjanczuk/haiku-http/blob/master/samples/haikus/hello.js. 41 | 42 | ## Samples 43 | 44 | You can explore and run more samples from https://github.com/tjanczuk/haiku-http/blob/master/samples/haikus/. Each file contains a haiku-http handler that implements one HTTP web API. These demonstrate making outbound HTTP[S] calls and getting data from a MongoDB database, among others. 45 | 46 | ## Using haiku-http runtime 47 | 48 | The haiku-http runtime is an HTTP and HTTPS server. It accepts any HTTP[S] request sent to it and returns a response generated by the haiku-http handler associated with that request. The caller controls the processing of the request with parameters attached to the HTTP request. Each named parameter can be provided either in the URL query string or as an HTTP request header. 49 | 50 | ### x-haiku-handler (required) 51 | 52 | The haiku-http handler to be used for processing the request must be specified by the caller by passing a URL of the handler in the `x-haiku-handler` parameter of the HTTP request. The haiku-http runtime will obtain the handler code by issuing an HTTP GET against the URL. It will then create a sanboxed execution environment for the handler code and pass the HTTP request to it for processing. The response generated by the handler is returned to the original caller. 53 | 54 | The simplest way to specify a haiku-http handler is to use URL query paramaters, e.g.: 55 | 56 | ``` 57 | http://localhost?x-haiku-handler=https://github.com/tjanczuk/haiku-http/blob/master/samples/haikus/hello.js 58 | ``` 59 | 60 | ### x-haiku-console (optional) 61 | 62 | The caller may use the `x-haiku-console` parameter to get access to the diagnostic information written out to the console by the haiku-http handler (e.g. using console.log). Following values of the paramater are allowed: 63 | 64 | - `none` (default) - console calls made by the haiku-http handler are allowed but ignored. 65 | - `body` - console output is captured and returned in the entity body of the HTTP response instead of the entity body generated by the haiku-http handler. Only console output generated until the call to `res.end()` is returned, all subsequent console output is discarded. 66 | - `header` - console output is captured and returned in the HTTP response header `x-haiku-console`. Entity body of the HTTP response as well as other HTTP response headers remain unaffected. Only console output generated until an explicit or implicit call to `res.writeHead()` is returned, all subsequent console output is discarded. 67 | - `trailer` - console output is captured and retuend in the HTTP response trailer `x-haiku-console`. Entity body of the HTTP response as well as other HTTP response headers remain unaffected. Only consle output generated until the call to `res.end()` is returned, all subsequent console output is discarded. Note that the debugging tools built into most browsers do not show HTTP trailers. One way to inspect the value of the HTTP trailer is to use `curl --trace -`. 68 | 69 | The simplest way to obtain the console output generated by the handler is to request it in the HTTP response body, e.g. (multiple lines for readability only): 70 | 71 | ``` 72 | http://localhost?x-haiku-handler=https://github.com/tjanczuk/haiku-http/blob/master/samples/haikus/console.js 73 | &x-haiku-console=body 74 | ``` 75 | 76 | For more examples of the various modes of accessing the console, check out the [console.js](https://github.com/tjanczuk/haiku-http/blob/master/samples/haikus/console.js) sample. 77 | 78 | ## Writing haiku-http handlers 79 | 80 | The haiku-http handler corresponds to the body of the HTTP reqeust callback function in node.js. 81 | 82 | For example, given the following node.js application: 83 | 84 | ``` 85 | require('http').createServer(function (req, res) { 86 | res.writeHead(200, {'Content-Type': 'text/plain'}); 87 | res.end('Hello World\n'); 88 | }).listen(1337, "127.0.0.1"); 89 | ``` 90 | 91 | a corresponding haiku-http handler would be: 92 | 93 | ``` 94 | res.writeHead(200, {'Content-Type': 'text/plain'}); 95 | res.end('Hello World\n'); 96 | ``` 97 | 98 | Within the haiku-http handler, the following globals are available: 99 | 100 | - `req` and `res` represent the HTTP request and response objects normally passed to the node.js request handler. Both objects are sandboxed. Check the section about sandboxing for details. 101 | - `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` work the same as in node.js. 102 | - `require` allows loading a node.js module from the subset available in the haiku-http sandbox environment. These include node.js core modules `http`, `https`, `net`, `tls`, `crypto`, `url`, `buffer`, `stream`, `events`, `util`, as well as third party modules `request`, and `mongodb`. All modules are sandboxed to only offer client side APIs (e.g. one can make outbound HTTP calls or TCP connections, but not establish a listener). Check the section about sandboxing for more details. 103 | 104 | Unlike in node.js, no global, in-memory state created by a haiku-http handler is preserved between subsequent calls. 105 | 106 | ## The haiku-http runtime model and configuration 107 | 108 | The haiku-http runtime behavior can be configured using command line parameters passed to the haiku-http.js application: 109 | 110 | ``` 111 | Usage: node ./haiku-http.js 112 | 113 | Options: 114 | -w, --workers Number of worker processes [default: 16] 115 | -p, --port HTTP listen port [default: 80] 116 | -s, --sslport HTTPS listen port [default: 443] 117 | -c, --cert Server certificate for SSL [default: "cert.pem"] 118 | -k, --key Private key for SSL [default: "key.pem"] 119 | -x, --proxy HTTP proxy in host:port format for outgoing requests [default: ""] 120 | -i, --maxsize Maximum size of a handler in bytes [default: "16384"] 121 | -t, --maxtime Maximum clock time in milliseconds for handler execution [default: "5000"] 122 | -r, --maxrequests Number of requests before process recycle. Zero for no recycling. [default: "1"] 123 | -a, --keepaliveTimout Maximum time in milliseconds to receive keepalive response from worker [default: "5000"] 124 | -v, --keepaliveInterval Interval between keepalive requests [default: "5000"] 125 | -l, --maxConsole Maximum console buffer in bytes. Zero for unlimited. [default: "4096"] 126 | -d, --debug Enable debug mode 127 | ``` 128 | 129 | Haiku-http uses node.js cluster functionality to load balance HTTP traffic across several worker processes. The `-w` parameter controls the number of worker processes in a cluster. When a worker process exits, the master will create a new instance to replace it. 130 | 131 | Recycling of a worker process can be forced after a specific number of HTTP requests has been processed by that worker. The number of HTTP requests is specified with the `-r` option, and defaults to 1. This means that by default OS processes are used to support haiku-http handler isolation, which is secure but not very efficient. Increasing that number (or disabling recycling altogether by setting the value to 0) increases the opportunity for rouge handlers to attempt local denial of service attacks (e.g. by blocking the node.js event loop), but also greatly improves performace of the system in case handlers are well behaved. Choosing the right value depends on the level of trust you put in the haiku-http handlers your installation runs. 132 | 133 | There are several limits that can be applied to the execution of the haiku-http handlers. First, the `-i` option controls the maximum size in bytes of the haiku-http handler, and is meant to limit the exposure of the runtime to attacks that cause downloading large amounts of data over the network. The `-t` option enforces a clock time limit on the execution of the handler (the time the handler takes to call the `res.end()` method). If the handler does not finish within this time limit, an HTTP error response is returned to the caller. Lastly, the `-l` option controls how much of the console output will be buffered in memory per each handler execution. 134 | 135 | The runtime cannot prevent runaway handlers (e.g. a handler that performs blocking operations) but it can detect such rouge handlers using a keepalive mechanism. At the interval specified by the `-v` option, the runtime will send a challenge to the worker process. If the worker process event loop is blocked, and it therefore does not respond to the challenge with the timeout specified with the `-a` option, the worker process is considered runaway, and is killed by the runtime. Subsequently the killed worker is replaced with a fresh instance, so the runtime recovers back to the original state. All requests that were active in that worker process are abnormally terminated, which means the number of reqeusts specified by the `-r` option is the upper limit of the exposure of the system to an execution of a rouge handler. 136 | 137 | The `-p` and `-s` options control the listen TCP ports for HTTP and HTTPS traffic, respectively. The `-c` and `-k` options specify the file name of the X.509 server certificate and corresponding private key, both in PEM format. 138 | 139 | The `-x` option allows you to specify the hostname and port number of an HTTP proxy to be used for obtaining haiku-http handler code from the URL specified by the `x-haiku-handler` parameter of an HTTP request. This is required in environments where a direct internet connection is not available. The proxy setting only applies to the haiku-http runtime behavior, not the behavior of the actual handler code. 140 | 141 | Lastly, the `-d` option cause any console output generated by the handler to be written out to stdout of the haiku-http.js process, in addition to processing it following client's disposition made with the `x-haiku-console` parameter of the HTTP request. 142 | 143 | ## The sandbox and isolation 144 | 145 | The sandbox used by the haiku-http runtime is essential in providing data isolation between haiku-http handlers. The principles behind the sandbox are following: 146 | 147 | - The sandbox limits available APIs to restrict data storage options to locations that do not rely on the operating system security mechanisms associated with the permissions of the haiku-http runtime process. As such handlers can exchange data with outside systems using HTTP and TCP, manipulate data in a MongoDB database, but they cannot otherwise access any data on the local machine or the network using permissions with which the http-haiku runtime executes. In particular, the `fs` module is not available. 148 | - The sandbox completely isolates in-memory data of a handler instance from other handler instances. This is accomplished by running each handler in its own V8 script context using a fresh instance of a global object, by disabling node.js module caching, and by preventing untusted code from accessing runtime's trusted core with the use of the ECMA script 5 'strict mode' when evaluating handler code. 149 | - The sandbox prevents handlers from creating new HTTP or TCP listeners, hence ensuring that invocation of any handler is subject to the sandboxing rules. In particular, rouge handlers cannot intercept requests that target other handlers. 150 | 151 | The haiku-http runtime provides a good level of data isolation at sub-process density using the sandboxing mechanisms above. However, running multiple handlers within the same process leaves room for a range of local denial of service attacks due to the current limits of the V8/node.js platform. You can read more about this aspect at [http://tomasz.janczuk.org](http://tomasz.janczuk.org). 152 | 153 | ## Running tests 154 | 155 | To run tests, first install the [mocha](https://github.com/visionmedia/mocha) test framework: 156 | 157 | ``` 158 | sudo npm install -g mocha 159 | ``` 160 | 161 | Then start the haiku-http runtime: 162 | 163 | ``` 164 | sudo node node_modules/haiku-http/src/haiku-http.js 165 | ``` 166 | 167 | As well as a local HTTP server that will serve the haiku-http handlers included in the repo: 168 | 169 | ``` 170 | node node_modules/haiku-http/samples/server.js 171 | ``` 172 | 173 | You are now ready to run tests: 174 | 175 | ``` 176 | mocha node_modules/haiku-http/test/* 177 | ``` 178 | 179 | If you experience errors, first make sure that test prerequisities are all met: 180 | 181 | ``` 182 | mocha -R spec node_modules/haiku-http/test/0* 183 | ``` 184 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haiku-http", 3 | "description": "Runtime for simple HTTP web APIs", 4 | "version": "0.2.0-pre", 5 | "author": { 6 | "name": "Tomasz Janczuk ", 7 | "url": "http://tomasz.janczuk.org", 8 | "twitter": "tjanczuk" 9 | }, 10 | "dependencies": { 11 | "optimist": "0.3.1", 12 | "mongodb": "0.9.8", 13 | "request": "2.9.100" 14 | }, 15 | "devDependencies": { 16 | }, 17 | "homepage": "http://github.com/tjanczuk/haiku-http", 18 | "bugs": { 19 | "url": "http://github.com/tjanczuk/haiku-http/issues" 20 | }, 21 | "keywords": ["http", "application", "web", "api"], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/tjanczuk/haiku-http.git" 25 | }, 26 | "engines": { 27 | "node": ">= 0.7.0-pre" 28 | }, 29 | "scripts": { 30 | "install": "node-waf configure build" 31 | } 32 | } -------------------------------------------------------------------------------- /samples/haikus/console.js: -------------------------------------------------------------------------------- 1 | // Write to the console and access console output. 2 | // 3 | // Try the following scenarios: 4 | // 5 | // Console output is ignored (default): 6 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/console.js 7 | // 8 | // Console output up until the end() call is returned in the body of the HTTP response instead of the actual body: 9 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/console.js&x-haiku-console=body 10 | // 11 | // Console output up until writeHead() call is returned in the x-haiku-console HTTP response header: 12 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/console.js&x-haiku-console=header 13 | // 14 | // Console output up until end() call is returned in the x-haiku-console HTTP response trailer: 15 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/console.js&x-haiku-console=body 16 | // (one way to view this one is with curl --trace -) 17 | 18 | console.log('Before writeHead'); 19 | res.writeHead(200); 20 | console.log('After writeHead and before write'); 21 | res.write('Hello, '); 22 | console.log('After write and before end'); 23 | res.end('world!\n'); 24 | console.log('After end'); -------------------------------------------------------------------------------- /samples/haikus/delayed-hello.js: -------------------------------------------------------------------------------- 1 | // Returns Hello, world in the response body in two parts, the second one delayed by 1 second. 2 | // 3 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/delayed-hello.js 4 | 5 | res.writeHead(200) 6 | res.write(new Date() + ': Hello, \n') 7 | setTimeout(function () { 8 | res.end(new Date() + ': world!\n') 9 | }, 1000) -------------------------------------------------------------------------------- /samples/haikus/hello.js: -------------------------------------------------------------------------------- 1 | // Returns Hello, world in the response body 2 | // 3 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/hello.js 4 | 5 | res.writeHead(200) 6 | res.end('Hello, world!\n') -------------------------------------------------------------------------------- /samples/haikus/https.js: -------------------------------------------------------------------------------- 1 | // Make outgoing HTTPS request for Github's home page and relay it back as a response. 2 | // 3 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/https.js 4 | 5 | require('https').get({ host: 'github.com' }, function (bres) { 6 | res.writeHead(bres.statusCode) 7 | bres.pipe(res) 8 | }).on('error', function (err) { 9 | res.writeHead(500) 10 | res.end('Error talking to backend: ' + err) 11 | }) -------------------------------------------------------------------------------- /samples/haikus/mongo.js: -------------------------------------------------------------------------------- 1 | // Connect to MongoDB instance at MongoHQ and return the result of a query. 2 | // The request URL can be parametrized with the 'host' parameter which 3 | // (if present) will be passed as filter to the MongoDB database. 4 | // 5 | // Try the following invocations for different results: 6 | // 7 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/mongo.js 8 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/mongo.js&host=app1.com 9 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/mongo.js&host=app2.com 10 | 11 | var query = require('url').parse(req.url, true).query 12 | var mongoUrl = query['db'] || 'mongodb://arr:arr@staff.mongohq.com:10024/arr' 13 | var filter = query['host'] ? { hosts: query['host'] } : {} 14 | 15 | require('mongodb').connect(mongoUrl, function (err, db) { 16 | if (notError(err)) 17 | db.collection('apps', function (err, apps) { 18 | if (notError(err)) 19 | apps.find(filter).toArray(function (err, docs) { 20 | if (notError(err)) { 21 | res.writeHead(200) 22 | res.end(JSON.stringify(docs)) 23 | } 24 | }) 25 | }) 26 | }) 27 | 28 | function notError(err) { 29 | if (err) { 30 | res.writeHead(500) 31 | res.end(err) 32 | } 33 | return !err 34 | } -------------------------------------------------------------------------------- /samples/haikus/proxied-https.js: -------------------------------------------------------------------------------- 1 | // Make outgoing HTTPS request through an HTTP proxy for Github's home page and relay it back as a response. 2 | // You must specify the proxy_host and proxy_port query parameters describing your HTTP proxy, e.g: 3 | // 4 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/proxied-https.js&proxy_host=itgproxy&proxy_port=80 5 | 6 | var query = require('url').parse(req.url, true).query 7 | 8 | if (!query.proxy_host || !query.proxy_port) 9 | throw new Error('The proxy_host and proxy_port URL query parameters must specify the HTTP proxy.') 10 | 11 | var http = require('http') 12 | , https = require('https') 13 | 14 | res.writeHead(200); 15 | 16 | http.request({ // establishing a tunnel 17 | host: query.proxy_host, 18 | port: query.proxy_port, 19 | method: 'CONNECT', 20 | path: 'github.com:443' 21 | }).on('connect', function(pres, socket, head) { 22 | if (pres.statusCode !== 200) 23 | res.end('Proxy response status code: ' + pres.statusCode); 24 | else 25 | https.get({ 26 | host: 'github.com', 27 | socket: socket, // using a tunnel 28 | agent: false // cannot use a default agent 29 | }, function (bres) { 30 | bres.pipe(res); 31 | }).on('error', function (err) { 32 | res.end('Error talking to backend: ' + err); 33 | }); 34 | }).on('error', function (err) { 35 | res.end('Error talking to proxy: ' + err); 36 | }).end(); -------------------------------------------------------------------------------- /samples/haikus/request.js: -------------------------------------------------------------------------------- 1 | 2 | // Use 'request' module to fetch http://reuters.com page and count the number of times 3 | // a specified word shows up on it. 4 | // 5 | // You can specify the word to count using the "word" URL query parameter. "the" is assumed when no word is specified. 6 | // You can also specify the proxy_host and proxy_port query parameters describing your HTTP proxy if you have one. 7 | // 8 | // Return the count of the "economy" word without using an HTTP proxy: 9 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/request.js&word=economy 10 | // 11 | // Return the count of the "economy" word without using itgproxy:80 HTTP proxy: 12 | // ?x-haiku-handler=https://raw.github.com/tjanczuk/haiku-http/master/samples/haikus/request.js&word=economy&proxy_host=itgproxy&proxy_port=80 13 | 14 | var query = require('url').parse(req.url, true).query 15 | var word = query.word || 'the' 16 | 17 | var request = require('request') 18 | 19 | if (query.proxy_host && query.proxy_port) 20 | request = request.defaults({ proxy: 'http://' + query.proxy_host + ':' + query.proxy_port }) 21 | 22 | request('http://www.reuters.com', function (error, response, body) { 23 | if (error || response.statusCode !== 200) { 24 | res.writeHead(500) 25 | res.end('Unexpected error getting http://reuters.com.\n') 26 | } 27 | else { 28 | var count = 0, index = 0 29 | while (0 !== (index = (body.indexOf(word, index) + 1))) 30 | count++ 31 | res.writeHead(200) 32 | res.end('Number of times the word "' + word + '" occurs on http://reuters.com is: ' + count + '\n') 33 | } 34 | }) -------------------------------------------------------------------------------- /samples/haikus/tests/entrypoint_EventEmitter.js: -------------------------------------------------------------------------------- 1 | var mongoUrl = 'mongodb://arr:arr@staff.mongohq.com:10024/arr' 2 | 3 | var result = [ haiku.getCurrentContextData() ] 4 | 5 | require('mongodb').connect(mongoUrl, function (err, db) { 6 | result.push(haiku.getCurrentContextData()) 7 | if (notError(err)) 8 | db.collection('apps', function (err, apps) { 9 | result.push(haiku.getCurrentContextData()) 10 | if (notError(err)) 11 | apps.find({}).toArray(function (err, docs) { 12 | result.push(haiku.getCurrentContextData()) 13 | if (notError(err)) { 14 | res.writeHead(200) 15 | res.end(JSON.stringify(result)) 16 | } 17 | }) 18 | }) 19 | }) 20 | 21 | function notError(err) { 22 | if (err) { 23 | res.writeHead(500) 24 | res.end(err) 25 | } 26 | return !err 27 | } -------------------------------------------------------------------------------- /samples/haikus/tests/entrypoint_nextTick.js: -------------------------------------------------------------------------------- 1 | res.writeHead(200) 2 | var result = [haiku.getCurrentContextData()] 3 | process.nextTick(function () { 4 | result.push(haiku.getCurrentContextData()) 5 | res.end(JSON.stringify(result)) 6 | }) -------------------------------------------------------------------------------- /samples/haikus/tests/entrypoint_setInterval.js: -------------------------------------------------------------------------------- 1 | res.writeHead(200) 2 | var result = [haiku.getCurrentContextData()] 3 | var i = setInterval(function () { 4 | clearInterval(i) 5 | result.push(haiku.getCurrentContextData()) 6 | res.end(JSON.stringify(result)) 7 | }, 1) -------------------------------------------------------------------------------- /samples/haikus/tests/entrypoint_setTimeout.js: -------------------------------------------------------------------------------- 1 | res.writeHead(200) 2 | var result = [haiku.getCurrentContextData()] 3 | setTimeout(function () { 4 | result.push(haiku.getCurrentContextData()) 5 | res.end(JSON.stringify(result)) 6 | }, 1) -------------------------------------------------------------------------------- /samples/server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | require('http').createServer(function (req, res) { 4 | try { 5 | console.log(__dirname + '/haikus' + req.url); 6 | file = fs.readFileSync(__dirname + '/haikus' + req.url + '.js'); 7 | res.writeHead(200); 8 | res.end(file); 9 | } 10 | catch (e) { 11 | res.writeHead(404); 12 | res.end(); 13 | } 14 | }).listen(8000) 15 | 16 | console.log('haiku-http sample server listening on http://localhost:8000. Ctrl-C to terminate.'); -------------------------------------------------------------------------------- /src/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICRTCCAa4CCQCywR0r8/wSgjANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJV 3 | UzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1JlZG1vbmQxITAfBgNVBAoTGEludGVy 4 | bmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAxQNKi5qYW5jenVrLm9yZzAeFw0x 5 | MjAyMDIxOTE5MjNaFw0xMjAzMDMxOTE5MjNaMGcxCzAJBgNVBAYTAlVTMQswCQYD 6 | VQQIEwJXQTEQMA4GA1UEBxMHUmVkbW9uZDEhMB8GA1UEChMYSW50ZXJuZXQgV2lk 7 | Z2l0cyBQdHkgTHRkMRYwFAYDVQQDFA0qLmphbmN6dWsub3JnMIGfMA0GCSqGSIb3 8 | DQEBAQUAA4GNADCBiQKBgQC6KNZ5b9Sb+n1zZ0ImhDEZST45m64tM+nLjDd6jfA1 9 | OBEdJo9hrYcqeEaiP3NneYGRUwWWtvcAuhXuajG0Df8RzGDJDUa7WlZIEZkXTUr9 10 | 9Ykixj8F85sQHk5sVsTfjCEw9bbqdS/uUMzjFcGGf+9r7qe1E2xOiDAwd5ZNMyr4 11 | yQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAKfimB87gNf/Jzn7KZ8B+lG+IZbTQq8Q 12 | fCXZph1oUx2mmYjXQwAn8gtCuu5TbBXng9UMaeFBD9UGX50MyTZf+jzgwdKCRH66 13 | m9CHYZmAsLr3zfoYNNoyiLfOaUM6FA2YNjfsGkLQk/yj4STOtv6SihtB8YghC6b/ 14 | fkPRx5Zpub7I 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /src/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBpzCCARACAQAwZzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQH 3 | EwdSZWRtb25kMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFjAU 4 | BgNVBAMUDSouamFuY3p1ay5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB 5 | ALoo1nlv1Jv6fXNnQiaEMRlJPjmbri0z6cuMN3qN8DU4ER0mj2Gthyp4RqI/c2d5 6 | gZFTBZa29wC6Fe5qMbQN/xHMYMkNRrtaVkgRmRdNSv31iSLGPwXzmxAeTmxWxN+M 7 | ITD1tup1L+5QzOMVwYZ/72vup7UTbE6IMDB3lk0zKvjJAgMBAAGgADANBgkqhkiG 8 | 9w0BAQUFAAOBgQAR7YaihbiEgrte1y0sKEvl8XCjyS2QNpwamgwV4nsWSarx3HH+ 9 | olrPe9kin/Teph0bWNbKeWdSfVj9SUdDABZ7GqRkaarfeh0RZpuUgYL7MutdeVK1 10 | Trx75v3F5yh08gaL3/qPoAvF64ceVqMe73f0x9HGUAHtV0EQ6/wA08+5LA== 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /src/haiku-http.js: -------------------------------------------------------------------------------- 1 | var cluster = require('cluster') 2 | , fs = require('fs') 3 | 4 | var argv = require('optimist') 5 | .usage('Usage: $0') 6 | .options('w', { 7 | alias: 'workers', 8 | description: 'Number of worker processes', 9 | default: require('os').cpus().length * 4 10 | }) 11 | .options('p', { 12 | alias: 'port', 13 | description: 'HTTP listen port', 14 | default: 80 15 | }) 16 | .options('s', { 17 | alias: 'sslport', 18 | description: 'HTTPS listen port', 19 | default: 443 20 | }) 21 | .options('c', { 22 | alias: 'cert', 23 | description: 'Server certificate for SSL', 24 | default: __dirname + '/cert.pem' 25 | }) 26 | .options('k', { 27 | alias: 'key', 28 | description: 'Private key for SSL', 29 | default: __dirname + '/key.pem' 30 | }) 31 | .options('x', { 32 | alias: 'proxy', 33 | description: 'HTTP proxy in host:port format for outgoing requests', 34 | default: '' 35 | }) 36 | .options('i', { 37 | alias: 'maxsize', 38 | description: 'Maximum size of a handler in bytes', 39 | default: '16384' 40 | }) 41 | .options('t', { 42 | alias: 'maxtime', 43 | description: 'Maximum clock time in milliseconds for handler execution', 44 | default: '5000' 45 | }) 46 | .options('r', { 47 | alias: 'maxrequests', 48 | description: 'Number of requests before process recycle. Zero for no recycling.', 49 | default: '1' 50 | }) 51 | .options('a', { 52 | alias: 'keepaliveTimout', 53 | description: 'Maximum time in milliseconds to receive keepalive response from worker', 54 | default: '5000' 55 | }) 56 | .options('v', { 57 | alias: 'keepaliveInterval', 58 | description: 'Interval between keepalive requests', 59 | default: '5000' 60 | }) 61 | .options('l', { 62 | alias: 'maxConsole', 63 | description: 'Maximum console buffer in bytes. Zero for unlimited.', 64 | default: '4096' 65 | }) 66 | .options('d', { 67 | alias: 'debug', 68 | description: 'Enable debug mode' 69 | }) 70 | .check(function (args) { return !args.help; }) 71 | .check(function (args) { return args.p != args.s; }) 72 | .check(function (args) { 73 | args.cert = fs.readFileSync(args.c); 74 | args.key = fs.readFileSync(args.k); 75 | return true; 76 | }) 77 | .check(function (args) { 78 | var proxy = args.x === '' ? process.env.HTTP_PROXY : args.x; 79 | if (proxy) { 80 | var i = proxy.indexOf(':'); 81 | args.proxyHost = i == -1 ? proxy : proxy.substring(0, i), 82 | args.proxyPort = i == -1 ? 80 : proxy.substring(i + 1) 83 | } 84 | return true; 85 | }) 86 | .argv; 87 | 88 | if (cluster.isMaster) 89 | require('./master.js').main(argv); 90 | else 91 | require('./worker.js').main(argv); 92 | -------------------------------------------------------------------------------- /src/haikuConsole.js: -------------------------------------------------------------------------------- 1 | var sandbox = require('./sandbox.js') 2 | , url = require('url') 3 | , util = require('util') 4 | 5 | function createDummyConsole(debugConsole) { 6 | var emptyFunction = function() {} 7 | var debugConsoleFunction = debugConsole ? function () { console.log.apply(console, arguments); } : emptyFunction; 8 | return { 9 | log: debugConsoleFunction, 10 | info: debugConsoleFunction, 11 | warn: debugConsoleFunction, 12 | error: debugConsoleFunction, 13 | trace: emptyFunction, 14 | assert: emptyFunction, 15 | dir: emptyFunction, 16 | time: emptyFunction, 17 | timeEnd: emptyFunction 18 | } 19 | } 20 | 21 | function createBufferingConsole(context, maxSize, debugConsole) { 22 | context.consoleBuffer = ''; 23 | 24 | var bufferedLog = function () { 25 | if (debugConsole) 26 | console.log.apply(console, arguments) 27 | if (context.consoleBuffer !== undefined) { // this is undefined once console has been sent to the client 28 | context.consoleBuffer += util.format.apply(this, arguments) + '\n'; 29 | if (context.consoleBuffer.length > maxSize) 30 | context.consoleBuffer = context.consoleBuffer.substring(context.buffer.length - maxSize); 31 | } 32 | } 33 | 34 | var emptyFunction = function() {} 35 | 36 | return { 37 | log: bufferedLog, 38 | info: bufferedLog, 39 | warn: bufferedLog, 40 | error: bufferedLog, 41 | trace: emptyFunction, 42 | assert: emptyFunction, 43 | dir: emptyFunction, 44 | time: emptyFunction, 45 | timeEnd: emptyFunction 46 | }; 47 | } 48 | 49 | function encodeConsole(console) { 50 | return url.format({query : { c : console }}).substring(3).replace(/%20/g, ' '); 51 | } 52 | 53 | function createConsole(context, maxSize, debugConsole) { 54 | 55 | var result; 56 | 57 | var onWrite = function () { 58 | if (!context.onWriteProcessed) { 59 | context.onWriteProcessed = true; 60 | if ('header' === context.console) { 61 | context.res.setHeader('x-haiku-console', encodeConsole(context.consoleBuffer)); 62 | delete context.consoleBuffer; 63 | } 64 | else if ('trailer' === context.console) 65 | context.res.setHeader('Trailer', 'x-haiku-console'); 66 | } 67 | 68 | if ('body' === context.console) 69 | return true; // ignore the application response 70 | else 71 | return arguments[--arguments.length].apply(this, arguments); 72 | } 73 | 74 | var onEnd = function () { 75 | if (!context.onEndProcessed) { 76 | context.onEndProcessed = true; 77 | var result; 78 | if ('trailer' === context.console) { 79 | context.res.addTrailers({'x-haiku-console': encodeConsole(context.consoleBuffer) }); 80 | result = arguments[--arguments.length].apply(this, arguments); 81 | } 82 | else // body 83 | result = arguments[--arguments.length].apply(this, [ context.consoleBuffer ]); 84 | 85 | delete context.consoleBuffer; 86 | 87 | return result; 88 | } 89 | else 90 | return arguments[--arguments.length].apply(this, arguments); 91 | } 92 | 93 | if ('header' === context.console) { 94 | result = createBufferingConsole(context, maxSize, debugConsole); 95 | context.res.writeHead = sandbox.wrapFunction(context.res, 'writeHead', onWrite); 96 | context.res.write = sandbox.wrapFunction(context.res, 'write', onWrite); 97 | context.res.end = sandbox.wrapFunction(context.res, 'end', onWrite); 98 | } 99 | else if ('trailer' === context.console) { 100 | result = createBufferingConsole(context, maxSize, debugConsole); 101 | context.res.writeHead = sandbox.wrapFunction(context.res, 'writeHead', onWrite); 102 | context.res.write = sandbox.wrapFunction(context.res, 'write', onWrite); 103 | context.res.end = sandbox.wrapFunction(context.res, 'end', onEnd); 104 | } 105 | else if ('body' === context.console) { 106 | result = createBufferingConsole(context, maxSize, debugConsole); 107 | context.res.write = sandbox.wrapFunction(context.res, 'write', onWrite); 108 | context.res.end = sandbox.wrapFunction(context.res, 'end', onEnd); 109 | } 110 | else 111 | result = createDummyConsole(debugConsole); 112 | 113 | return result; 114 | } 115 | 116 | exports.createConsole = createConsole; -------------------------------------------------------------------------------- /src/haiku_extensions.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace v8; 10 | 11 | // Thread CPU time measurements: 12 | // - with pthreads: http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_getcpuclockid.3.html 13 | // - on Mach: http://stackoverflow.com/questions/6788274/ios-mac-cpu-usage-for-thread 14 | 15 | thread_t nodeThread; 16 | unsigned int threshold = 0; 17 | pthread_mutex_t mutex; 18 | Persistent currentCtx; 19 | bool watchdogActive = false; 20 | bool inUserCode = false; 21 | int userCodeDepth = 0; 22 | // struct timespec enterTime; 23 | // clockid_t nodeClockId = 0; 24 | double enterTime; // in seconds 25 | 26 | int GetNodeThreadCPUTime(double* s) { 27 | if (!s) 28 | return -1; 29 | 30 | int error; 31 | thread_info_data_t threadInfo; 32 | mach_msg_type_number_t count; 33 | 34 | if (0 != (error = thread_info(nodeThread, THREAD_BASIC_INFO, (thread_info_t)threadInfo, &count))) 35 | return error; 36 | 37 | thread_basic_info_t basicThreadInfo = (thread_basic_info_t)threadInfo; 38 | 39 | if (basicThreadInfo->flags & TH_FLAGS_IDLE) 40 | *s = 0; 41 | else 42 | *s = (double)(basicThreadInfo->user_time.seconds + basicThreadInfo->system_time.seconds) 43 | + (double)(basicThreadInfo->user_time.microseconds + basicThreadInfo->system_time.microseconds) / 10e5; 44 | 45 | return 0; 46 | } 47 | 48 | Handle SetContextDataOn(const Arguments& args) { 49 | HandleScope scope; 50 | 51 | if (args.Length() != 2 || !args[0]->IsObject() || !args[1]->IsString()) 52 | return ThrowException(Exception::TypeError(String::New("The first parameter must be an object and the second a string."))); 53 | 54 | Local data = args[0]->ToObject()->CreationContext()->GetData(); 55 | 56 | if (data->IsString() && data->ToString() != args[1]->ToString()) { 57 | printf("SetContextDataOn: current data on object's creation context: %s, new data: %s\n", 58 | *String::Utf8Value(data), 59 | *String::Utf8Value(args[1]->ToString())); 60 | return ThrowException(Exception::Error(String::New("Object's creation context already has a different context data set."))); 61 | } 62 | 63 | args[0]->ToObject()->CreationContext()->SetData(args[1]->ToString()); // TODO should this be a persistent handle? 64 | 65 | return Undefined(); 66 | } 67 | 68 | Handle GetContextDataOf(const Arguments& args) { 69 | HandleScope scope; 70 | 71 | if (args.Length() != 1 || !args[0]->IsObject()) 72 | return ThrowException(Exception::TypeError(String::New("The first parameter must be an object."))); 73 | 74 | return args[0]->ToObject()->CreationContext()->GetData(); 75 | } 76 | 77 | Handle EnterContextOf(const Arguments& args) { 78 | HandleScope scope; 79 | 80 | if (args.Length() != 1 || !args[0]->IsObject()) 81 | return ThrowException(Exception::TypeError(String::New("The first parameter must be an object."))); 82 | 83 | args[0]->ToObject()->CreationContext()->Enter(); 84 | 85 | return Undefined(); 86 | } 87 | 88 | Handle ExitContextOf(const Arguments& args) { 89 | HandleScope scope; 90 | 91 | if (args.Length() != 1 || !args[0]->IsObject()) 92 | return ThrowException(Exception::TypeError(String::New("The first parameter must be an object."))); 93 | 94 | args[0]->ToObject()->CreationContext()->Exit(); 95 | 96 | return Undefined(); 97 | } 98 | 99 | Handle GetCurrentContextData(const Arguments& args) { 100 | return currentCtx; 101 | } 102 | 103 | void* Watchdog(void *data) { 104 | unsigned int remain = threshold; 105 | while (true) { 106 | remain = sleep(remain); 107 | if (0 == remain) { 108 | pthread_mutex_lock(&mutex); 109 | if (inUserCode && watchdogActive) { 110 | // struct timespec now; 111 | // if (-1 != clock_gettime(nodeClockId, &now)) { 112 | // if ((now.tv_sec - enterTime.tv_sec) >= threshold) { 113 | // watchdogActive = false; 114 | // V8::TerminateExecution(); 115 | // } 116 | // } 117 | double now; 118 | if (0 == GetNodeThreadCPUTime(&now)) { 119 | if ((now - enterTime) >= threshold) { 120 | watchdogActive = false; 121 | V8::TerminateExecution(); 122 | } 123 | } 124 | } 125 | pthread_mutex_unlock(&mutex); 126 | remain = threshold; 127 | } 128 | } 129 | return NULL; 130 | } 131 | 132 | Handle StartWatchdog(const Arguments& args) { 133 | HandleScope scope; 134 | 135 | if (0 != threshold) 136 | return ThrowException(Exception::Error(String::New("Watchdog has already been started."))); 137 | 138 | if (1 != args.Length()) 139 | return ThrowException(Exception::Error(String::New("One argument is required."))); 140 | 141 | if (!args[0]->IsUint32()) 142 | return ThrowException(Exception::TypeError(String::New("First argument must be a time threshold for blocking operations in seconds."))); 143 | 144 | threshold = args[0]->ToUint32()->Value(); 145 | 146 | if (0 == threshold) 147 | return ThrowException(Exception::Error(String::New("The time threshold for blocking operations must be greater than 0."))); 148 | 149 | if (/*0 != pthread_getcpuclockid(pthread_self(), &nodeClockId) 150 | ||*/ 0 != pthread_mutex_init(&mutex, NULL) 151 | || 0 != pthread_create(NULL, NULL, Watchdog, NULL)) { 152 | threshold = 0; 153 | return ThrowException(Exception::Error(String::New("Error creating watchdog thread."))); 154 | } 155 | 156 | return Undefined(); 157 | } 158 | 159 | Handle EnterUserCode(const Arguments& args) { 160 | HandleScope scope; 161 | const char* exception = NULL; 162 | if (0 == userCodeDepth) { 163 | pthread_mutex_lock(&mutex); 164 | 165 | // if (-1 != (error = clock_gettime(nodeClockId, &enterTime))) { 166 | /*if (0 != GetNodeThreadCPUTime(&enterTime)) 167 | exception = "Internal Error. Unable to capture thread CPU time."; 168 | else */ if (1 != args.Length()) 169 | exception = "One parameter required."; 170 | else if (!args[0]->IsObject()) 171 | exception = "The parameter must be an object (created in user code context)."; 172 | else if (!args[0]->ToObject()->CreationContext()->GetData()->IsString()) 173 | exception = "The specified object's creation context does not have context data set."; 174 | else { 175 | // printf("enter user code: %s\n", *String::Utf8Value(args[0]->ToObject()->CreationContext()->GetData()->ToString())); 176 | currentCtx = Persistent::New(args[0]->ToObject()->CreationContext()->GetData()->ToString()); 177 | inUserCode = true; 178 | watchdogActive = true; 179 | } 180 | 181 | pthread_mutex_unlock(&mutex); 182 | } 183 | 184 | if (exception) 185 | return ThrowException(Exception::Error(String::New(exception))); 186 | else { 187 | userCodeDepth++; 188 | return Undefined(); 189 | } 190 | } 191 | 192 | Handle LeaveUserCode(const Arguments& args) { 193 | HandleScope scope; 194 | 195 | if (1 == userCodeDepth) { 196 | // printf("leave user code\n"); 197 | pthread_mutex_lock(&mutex); 198 | inUserCode = false; 199 | currentCtx.Dispose(); 200 | currentCtx.Clear(); 201 | pthread_mutex_unlock(&mutex); 202 | } 203 | 204 | userCodeDepth--; 205 | 206 | return Undefined(); 207 | } 208 | 209 | Handle RunInObjectContext(const Arguments& args) { 210 | HandleScope scope; 211 | 212 | if (args.Length() != 2 || !args[0]->IsObject() || !args[1]->IsFunction()) 213 | return ThrowException(Exception::TypeError(String::New("The first parameter must be an object and the second a function to run in that object's creation context."))); 214 | 215 | Handle global = args[0]->ToObject()->CreationContext()->Global(); 216 | Context::Scope contextScope(args[0]->ToObject()->CreationContext()); 217 | Handle result = Local::Cast(args[1])->Call(global, 0, NULL); 218 | 219 | return scope.Close(result); 220 | } 221 | 222 | Handle Printf(const Arguments& args) { 223 | printf("%s\n", *String::Utf8Value(args[0]->ToString())); 224 | return Undefined(); 225 | } 226 | 227 | extern "C" 228 | void init(Handle target) { 229 | nodeThread = mach_thread_self(); 230 | target->Set(String::NewSymbol("setContextDataOn"), FunctionTemplate::New(SetContextDataOn)->GetFunction()); 231 | target->Set(String::NewSymbol("getCurrentContextData"), FunctionTemplate::New(GetCurrentContextData)->GetFunction()); 232 | target->Set(String::NewSymbol("getContextDataOf"), FunctionTemplate::New(GetContextDataOf)->GetFunction()); 233 | target->Set(String::NewSymbol("startWatchdog"), FunctionTemplate::New(StartWatchdog)->GetFunction()); 234 | target->Set(String::NewSymbol("enterUserCode"), FunctionTemplate::New(EnterUserCode)->GetFunction()); 235 | target->Set(String::NewSymbol("leaveUserCode"), FunctionTemplate::New(LeaveUserCode)->GetFunction()); 236 | target->Set(String::NewSymbol("runInObjectContext"), FunctionTemplate::New(RunInObjectContext)->GetFunction()); 237 | target->Set(String::NewSymbol("printf"), FunctionTemplate::New(Printf)->GetFunction()); 238 | } 239 | NODE_MODULE(haiku_extensions, init) 240 | -------------------------------------------------------------------------------- /src/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC6KNZ5b9Sb+n1zZ0ImhDEZST45m64tM+nLjDd6jfA1OBEdJo9h 3 | rYcqeEaiP3NneYGRUwWWtvcAuhXuajG0Df8RzGDJDUa7WlZIEZkXTUr99Ykixj8F 4 | 85sQHk5sVsTfjCEw9bbqdS/uUMzjFcGGf+9r7qe1E2xOiDAwd5ZNMyr4yQIDAQAB 5 | AoGAeVJaDIR0RD8oeQhnlSB7uyX/tp2eEvmNOcmk8msEjDqA9MWHljn4KBaAugau 6 | GFaYuXQo5UNSkJe16U4uHFEu1HWTQ4OkVjhJ+CacyaZcJV54KT6N4hC8+TiN1Y/b 7 | 4y1TWxHvFByY6+MI6/ZcqdkyzvqTsl0EM3+7CBeSv5Ia2U0CQQDyFzcsxaXRi+9z 8 | s8jmIiswIYAGcswCJe5u1a3sgACW5AkHRil5FG3s3PDtFcpTLJlDqILYIIRiT4+D 9 | 26vOpT4/AkEAxNr10VQylRgwzXsMDpjCNm6k6J7eJRToZljpAjn30aBvODeGaNu0 10 | 98+PxPd4mhVfw9lXBGI7tNTsh6gBxf2W9wJBANrFT/8NvYNXydPtLCeLySt9mow5 11 | QVLPpGBUiQ+nvOCeweno5aGdbJkYMECP6H6xVu9lYJifCgMtkqu938ymV1ECQFs6 12 | rllIj/iQsW1I7RmGqdrYBAzaM1E0E0/7PGEPxE2d8G05Lk1CJOgDhTlfBsFBzpPR 13 | EYayj8EKPGPR9KBxGZkCQAEA1hwpb4uk6EIv67SiSN09yy5qISJMRH9gyc6AlZmw 14 | U7ryfkIU8oiJGiYh6C63NoUS8i0ZTZaahyxNxlX9v5I= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /src/master.js: -------------------------------------------------------------------------------- 1 | var cluster = require('cluster'); 2 | var argv; 3 | 4 | function log(thing) { 5 | console.log(process.pid + ' (master): ' + thing); 6 | } 7 | 8 | function challange(worker) { 9 | worker.challangeTimeout = setTimeout(function() { 10 | delete worker.challangeTimeout; 11 | worker.currentChallange = Math.random(); 12 | worker.send({ challange: worker.currentChallange }); 13 | worker.keepaliveTimeout = setTimeout(function () { 14 | log('Worker ' + worker.process.pid + ' did not respond to keepalive challange within ' + argv.a + 'ms. Killing the process.'); 15 | process.kill(worker.process.pid); 16 | }, argv.a); // the keepalive response timeout 17 | }, argv.v); // the keepalive interval 18 | } 19 | 20 | function stopKeepalive(worker) { 21 | if (worker.keepaliveTimeout) { 22 | clearTimeout(worker.keepaliveTimeout); 23 | delete worker.keepaliveTimeout; 24 | } 25 | 26 | if (worker.challangeTimeout) { 27 | clearTimeout(worker.challangeTimeout); 28 | delete worker.challangeTimeout; 29 | } 30 | } 31 | 32 | function createOneWorker() { 33 | var worker = cluster.fork(); 34 | 35 | worker.on('message', function(msg) { 36 | stopKeepalive(worker); 37 | if (msg.response !== worker.currentChallange) { 38 | log('Worker ' + worker.process.pid + ' sent incorrect response to keepalive challange. Killing the process.'); 39 | process.kill(worker.process.pid); 40 | } 41 | else 42 | challange(worker); 43 | }); 44 | 45 | challange(worker); 46 | } 47 | 48 | exports.main = function (args) { 49 | argv = args; 50 | log('haiku-http: a runtime for simple HTTP web APIs'); 51 | log('Number of workers: ' + argv.w); 52 | log('HTTP port: ' + argv.p); 53 | log('HTTPS port: ' + argv.s); 54 | log('HTTP proxy: ' + (argv.proxyHost ? (argv.proxyHost + ':' + argv.proxyPort) : 'none')); 55 | log('Max handler size [bytes]: ' + argv.i); 56 | log('Max handler execution time [ms]: ' + argv.t); 57 | log('Max requests before recycle: ' + argv.r); 58 | log('Keepalive response timeout [ms]: ' + argv.a); 59 | log('Keepalive interval [ms]: ' + argv.v); 60 | log('Debug mode: ' + (argv.d ? 'yes' : 'no')); 61 | 62 | for (var i = 0; i < argv.w; i++) 63 | createOneWorker(); 64 | 65 | cluster.on('death', function (worker) { 66 | log('Worker ' + worker.process.pid + ' exited, creating replacement worker.'); 67 | stopKeepalive(worker); 68 | createOneWorker(); 69 | }); 70 | 71 | log('haiku-http started. Ctrl-C to terminate.'); 72 | } 73 | -------------------------------------------------------------------------------- /src/sandbox.js: -------------------------------------------------------------------------------- 1 | var OriginalEventEmitter = require('events').EventEmitter 2 | , haiku_extensions = require('./build/Release/haiku_extensions.node') 3 | , module = require('module') 4 | , vm = require('vm') 5 | 6 | // create EventEmitter interceptor that wraps original EventEmitter to 7 | // intercept callbacks into user code and trigger the CPU watchdog 8 | // and per user CPU consumption measurements in the haiku-http runtime 9 | 10 | // TODO: event emitter should be compiled anew in the user V8 context rather than 11 | // constructed by inheriting from the one created in main context to avoid shring global state. 12 | // See #11. 13 | 14 | function EventEmitter() { 15 | 16 | var self = this 17 | 18 | OriginalEventEmitter.call(self) 19 | 20 | // 'emit' property cannot be deleted because it is non-writable by default 21 | // so user code cannot avoid entering the sandbox 22 | 23 | Object.defineProperty(this, 'emit', { 24 | enumerable: true, 25 | value: function(type) { 26 | var handlers = self.listeners(type) 27 | var userCode = handlers[0] 28 | if (typeof userCode === 'function') { 29 | userCode = userCode.listener || userCode 30 | if (typeof userCode === 'function') { 31 | haiku_extensions.enterUserCode(userCode) 32 | } 33 | } 34 | try { 35 | return OriginalEventEmitter.prototype.emit.apply(self, arguments) 36 | } 37 | finally { 38 | if (typeof userCode === 'function') 39 | haiku_extensions.leaveUserCode() 40 | } 41 | } 42 | }) 43 | 44 | } 45 | 46 | require('util').inherits(EventEmitter, OriginalEventEmitter) 47 | 48 | // defines the subset of module functionality that will be exposed 49 | // to the haiku-http handler via the "require" function 50 | 51 | var moduleSandbox = { 52 | 53 | // subset fuctionality of these modules to only expose client side APIs 54 | // as needed, wrap APIs to sandbox inputs or outputs 55 | 56 | http : { 57 | request: wrapHttpRequest, 58 | get: wrapHttpRequest, 59 | Agent: true 60 | }, 61 | 62 | https : { 63 | request: wrapHttpRequest, 64 | get: wrapHttpRequest 65 | }, 66 | 67 | tls : { 68 | createSecurePair: true, 69 | connect: true 70 | }, 71 | 72 | net : { 73 | Stream: true, 74 | createConnection: true, 75 | connect: true 76 | }, 77 | 78 | // limit crypto surface area to support implementation closure of other modules 79 | 80 | crypto : { 81 | createHash: true // required by MongoDB 82 | }, 83 | 84 | // intercept entry points from node.js event loop into user JavaScript code that use EventEmitter 85 | 86 | events: { 87 | EventEmitter: function () { return EventEmitter; } 88 | }, 89 | 90 | // secrity treat as safe APIs 91 | 92 | url : true, 93 | util : true, 94 | buffer : true, 95 | stream : true, 96 | 97 | // security transparent APIs 98 | 99 | mongodb : true, 100 | request : true, 101 | 102 | // unsafe APIs that must be stubbed out 103 | 104 | fs : { 105 | // MongoDB library requires this module for GridStore scenarios, 106 | // but they are not relevant for haiku-http, so the fs module instance never gets used 107 | } 108 | } 109 | 110 | // defines properties from http.ClientRequest (own and inherited) that will be 111 | // exposed to the haiku-http handler 112 | 113 | var clientRequestSandbox = { 114 | writable: true, 115 | write: true, 116 | end: true, 117 | abort: true, 118 | setTimeout: true, 119 | setNoDelay: true, 120 | setSocketKeepAlive: true, 121 | pipe: true, 122 | addListener: wrapResponseEvent, 123 | on: wrapResponseEvent, 124 | once: wrapResponseEvent, 125 | removeListener: true, 126 | removeAllListeners: true, 127 | setMaxListeners: true, 128 | // listeners: true, 129 | emit: true 130 | } 131 | 132 | // defines properties from http.ClientResponse (own and inherited) that will be 133 | // exposed to the haiku-http handler 134 | 135 | var clientResposeSandbox = { 136 | readable: true, 137 | statusCode: true, 138 | httpVersion: true, 139 | headers: true, 140 | trailers: true, 141 | setEncoding: true, 142 | pause: true, 143 | resume: true, 144 | pipe: true, 145 | addListener: true, 146 | on: true, 147 | once: true, 148 | removeListener: true, 149 | removeAllListeners: true, 150 | setMaxListeners: true, 151 | // listeners: true, 152 | emit: true 153 | } 154 | 155 | // defines properties from http.ServerRequest (own and inherited) that will be 156 | // exposed to the haiku-http handler 157 | 158 | var serverRequestSandbox = { 159 | readable: true, 160 | method: true, 161 | url: true, 162 | headers: true, 163 | trailers: true, 164 | httpVersion: true, 165 | setEncoding: true, 166 | pause: true, 167 | resume: true, 168 | pipe: true, 169 | addListener: true, 170 | on: true, 171 | once: true, 172 | removeListener: true, 173 | removeAllListeners: true, 174 | setMaxListeners: true, 175 | // listeners: true, 176 | emit: true 177 | } 178 | 179 | // defines properties from http.ServerResponse (own and inherited) that will be 180 | // exposed to the haiku-http handler 181 | 182 | var serverResponseSandbox = { 183 | writable: true, 184 | writeHead: true, 185 | statusCode: true, 186 | removeHeader: true, 187 | write: true, 188 | addTrailers: true, 189 | end: true, 190 | addListener: true, 191 | on: true, 192 | once: true, 193 | removeListener: true, 194 | removeAllListeners: true, 195 | setMaxListeners: true, 196 | // listeners: true, 197 | emit: true 198 | } 199 | 200 | // wrap a function on an object with another function 201 | // the wrapped function will be passed as the last argument to the wrapping function 202 | // wrapping function is called in the context of the instance the wrapped function belongs to 203 | 204 | function wrapFunction(instance, func, wrapperFunc) { 205 | var oldFunc = instance[func]; 206 | return function () { 207 | arguments[arguments.length++] = oldFunc; 208 | return wrapperFunc.apply(instance, arguments); 209 | } 210 | } 211 | 212 | // wrap http.{request|get} to return a sandboxed http.ClientRequest 213 | 214 | function wrapHttpRequest(object, parent, nameOnParent, executionContext) { 215 | return wrapFunction(parent, nameOnParent, function () { 216 | var clientRequest = arguments[--arguments.length].apply(this, arguments); 217 | return createObjectSandbox(clientRequestSandbox, clientRequest); 218 | }); 219 | } 220 | 221 | // wrap http.ClientRequest.{on|once|addListener}('response', ...) to return a sandboxed http.ClientResponse 222 | 223 | function wrapResponseEvent(object, parent, nameOnParent, executionContext) { 224 | return wrapFunction(parent, nameOnParent, function (type, listener) { 225 | var oldFunc = arguments[--arguments.length]; 226 | if ('response' === type) { 227 | // intercept 'response' event subscription and sandbox the response 228 | // TODO this wrapping will make removeListener break 229 | oldFunc('request', function(res) { 230 | listener(createObjectSandbox(clientResposeSandbox, res)); 231 | }) 232 | } 233 | else 234 | // pass-through for all other event types 235 | return oldFunc.apply(this, arguments); 236 | }); 237 | } 238 | 239 | function createObjectSandbox(sandbox, object, parent, nameOnParent, executionContext) { 240 | // haiku_extensions.printf('in createObjectSandbox ' + sandbox + ' ' + object) 241 | if (typeof sandbox === 'function') { 242 | // custom sandboxing logic 243 | return sandbox(object, parent, nameOnParent, executionContext); 244 | } 245 | else if (true === sandbox) { 246 | if (typeof object === 'function' && executionContext) 247 | // ensure the function is invoked in the correct execution context 248 | return function () { return object.apply(executionContext, arguments); } 249 | else 250 | // "security treat as safe", return back without wrapping 251 | return object; 252 | } 253 | else { 254 | 255 | // Sandbox properties owned by object and properties inherited from the prototype chain 256 | // this flattens out the properties inherited from the prototype chain onto 257 | // a single result object. Any code that depends on the existence of the prototype chain 258 | // will likely be broken by this, but any code that just invokes the members should continue 259 | // working. 260 | 261 | var result = {}; 262 | var current = object; 263 | while (current) { 264 | for (var element in sandbox) 265 | if (!result[element] && current[element]) // preserve inheritance chain 266 | result[element] = createObjectSandbox(sandbox[element], current[element], current, element, object); 267 | current = Object.getPrototypeOf(current); 268 | } 269 | 270 | return result; 271 | } 272 | } 273 | 274 | function passthroughFunctionWrap(func) { 275 | return function () { return func.apply(func, arguments); } 276 | } 277 | 278 | function userFunctionWrap(func) { 279 | return function () { 280 | if (typeof func === 'function') 281 | haiku_extensions.enterUserCode(func) 282 | try { 283 | return func.apply(this, arguments) 284 | } 285 | finally { 286 | if (typeof func === 'function') 287 | haiku_extensions.leaveUserCode() 288 | } 289 | } 290 | } 291 | 292 | function createSandbox(context, addons) { 293 | 294 | // expose sandboxed 'require', request, and response, plus some useful globals 295 | 296 | context.sandbox = { 297 | require: passthroughFunctionWrap(require), // sandboxing of module system happens in enterModuleSandbox() below 298 | setTimeout: passthroughFunctionWrap(setTimeout), 299 | clearTimeout: passthroughFunctionWrap(clearTimeout), 300 | setInterval: passthroughFunctionWrap(setInterval), 301 | clearInterval: passthroughFunctionWrap(clearInterval), 302 | req: createObjectSandbox(serverRequestSandbox, context.req), 303 | res: createObjectSandbox(serverResponseSandbox, context.res), 304 | haiku: { 305 | getContextDataOf: passthroughFunctionWrap(haiku_extensions.getContextDataOf), 306 | getCurrentContextData: passthroughFunctionWrap(haiku_extensions.getCurrentContextData) 307 | }, 308 | process: { 309 | nextTick: passthroughFunctionWrap(process.nextTick) 310 | } 311 | }; 312 | 313 | // add custom add-ons to the sandbox (e.g. 'console') 314 | 315 | if (addons) 316 | for (var i in addons) 317 | context.sandbox[i] = addons[i]; 318 | 319 | return context.sandbox; 320 | } 321 | 322 | function enterModuleSandbox(NativeModule) { 323 | 324 | var global = (function () { return this; }).call(null) 325 | 326 | // Sandbox process.nextTick as an entry point to user code 327 | 328 | oldNextTick = process.nextTick 329 | process.nextTick = function (func) { 330 | return oldNextTick(userFunctionWrap(func)) 331 | } 332 | 333 | // Sandbox setTimeout as an entry point to user code 334 | 335 | oldSetTimeout = global.setTimeout 336 | global.setTimeout = function () { 337 | var newArguments = [ userFunctionWrap(arguments[0]) ]; 338 | for (var i = 1; i < arguments.length; i++) 339 | newArguments.push(arguments[i]) 340 | return oldSetTimeout.apply(this, newArguments) 341 | } 342 | 343 | // Sandbox setInterval as an entry point to user code 344 | 345 | oldSetInterval = global.setInterval 346 | global.setInterval = function () { 347 | var newArguments = [ userFunctionWrap(arguments[0]) ]; 348 | for (var i = 1; i < arguments.length; i++) 349 | newArguments.push(arguments[i]) 350 | return oldSetInterval.apply(this, newArguments) 351 | } 352 | 353 | // Sandbox the module system 354 | 355 | var inRequireEpisode = false 356 | 357 | // Force all native modules to be loaded in their own context. 358 | // Native modules are by default loaded in the main V8 context. We need to override this logic 359 | // to load native modules in their own V8 context instead, such that each copy of a native module 360 | // can be assigned to one particular handler instance 361 | 362 | var oldRequire = NativeModule.require 363 | 364 | NativeModule.require = function () { 365 | var enteredRequireEpisode = false; 366 | var contextHook 367 | if (!inRequireEpisode) { 368 | inRequireEpisode = enteredRequireEpisode = true; 369 | for (var i in NativeModule._cache) 370 | delete NativeModule._cache[i]; 371 | } 372 | try { 373 | return oldRequire.apply(this, arguments); 374 | } 375 | finally { 376 | if (enteredRequireEpisode) 377 | inRequireEpisode = false 378 | } 379 | } 380 | 381 | // Modify all native modules to add __haiku_hook object to their exports. 382 | // This object will be created in the V8 context in which the module is loaded. 383 | // This object is used in the NativeModule.prototype.compile overload 384 | // to associate context data with the module context. 385 | 386 | // See comment in module.prototype._compile to understand the following logic 387 | 388 | NativeModule.wrapper[0] += ' module.exports.__haiku_hook = {}; ' 389 | 390 | NativeModule.prototype.compile = function() { 391 | var source = NativeModule.getSource(this.id) 392 | source = NativeModule.wrap(source); 393 | 394 | var ctx = vm.createContext(global) 395 | var fn = vm.runInContext(source, ctx, this.filename) 396 | fn(this.exports, NativeModule.require, this, this.filename) 397 | 398 | var contextDataSet 399 | var currentContextData = haiku_extensions.getCurrentContextData() 400 | var contextHook = this.exports['__haiku_hook'] || this.exports 401 | if (currentContextData) { 402 | if (typeof contextHook !== 'object' && typeof contextHook !== 'function') 403 | throw new Error('Internal error. Unable to determine the context hook of the native module ' + this.id + '.') 404 | 405 | // Some native modules (e.g. constants) expose bindings created in main context; treat them as trusted. 406 | // TODO: this is a potential security issue if such objects leak to the user space. See #10. 407 | 408 | if (haiku_extensions.getContextDataOf(contextHook) !== 'MainContext') { 409 | haiku_extensions.setContextDataOn(contextHook, currentContextData) 410 | contextDataSet = true 411 | } 412 | } 413 | 414 | if (this.id === 'events' && moduleSandbox[this.id] && contextDataSet) { 415 | var sandbox = moduleSandbox[this.id] 416 | var objectToSandbox = this.exports 417 | this.exports = haiku_extensions.runInObjectContext( 418 | contextHook, 419 | function () { 420 | return createObjectSandbox(sandbox, objectToSandbox) 421 | } 422 | ) 423 | } 424 | 425 | this.loaded = true; 426 | } 427 | 428 | // Force all user modules to be loaded in their own V8 context. 429 | // This is used to establish association of any executing code with a particular 430 | // handler invocation that loaded that code by assigning a custom identifier 431 | // to the V8 context's context data field. 432 | 433 | module._contextLoad = true 434 | 435 | var originalCompile = module.prototype._compile 436 | 437 | module.prototype._compile = function (content, filename) { 438 | // remove shebang 439 | content = content.replace(/^\#\!.*/, ''); 440 | 441 | // Modify module content to add __haiku_hook object to the exports. 442 | // This object will be created in the V8 context in which the module is loaded. 443 | // This object is used in the Module._load overload to associate context data 444 | // with the module context. 445 | 446 | // This is a bit tricky for 2 reasons: 447 | // 1. The __haiku_hook must be created on the module's exports before any 448 | // other submodules are loaded, since sub-modules may have cyclic 449 | // dependencies. If such a dependency exists, module._load must be able to 450 | // access the __haiku_hook property of the partially constructed module; 451 | // this is why __haiku_hook is created first thing in the module code. 452 | // 2. Some modules replace the entire module.exports object with its own 453 | // rather than adding properties to it. For such modules the __haiku_hook 454 | // property will be absent in module._load - in that case module_load 455 | // will attempt to fall back on the module.exports object as an object created in 456 | // in the V8 creation context of the module. 457 | 458 | content = 'module.exports.__haiku_hook = {};' + content 459 | var result = originalCompile.call(this, content, filename); 460 | 461 | return result; 462 | } 463 | 464 | var originalLoad = module._load 465 | 466 | module._load = function (request, parent, isMain) { 467 | 468 | // 'Require episode' is a synchronous module resolution code path initiated 469 | // with the topmost invocation of 'require' method on the call stack. 470 | // A require episode may recursively invoke 'require' again. 471 | // The purpose of the machanism below is to limit module caching to a single 472 | // 'require episode' to: 473 | // - support cyclic module dependencies within a single require episode, 474 | // - avoid sharing module instances across several haiku handlers. 475 | // This is achieved by removing all cached modules at the beginning of every 476 | // require episode. 477 | 478 | // TODO: investigate ways of scoping module caching to a single haiku handler 479 | // (script context) for improved performance. 480 | 481 | var enteredRequireEpisode = false 482 | if (!inRequireEpisode) { 483 | inRequireEpisode = enteredRequireEpisode = true 484 | for (var i in module._cache) 485 | delete module._cache[i] 486 | for (var i in NativeModule._cache) 487 | delete NativeModule._cache[i] 488 | } 489 | 490 | try { 491 | if (!moduleSandbox[request] && !request[0] === '.' && !request === 'querystring') 492 | // request module requires its own 'querystring' without a dot 493 | throw 'Module ' + request + ' is not available in the haiku-http sandbox.' 494 | 495 | var result = originalLoad(request, parent, isMain) 496 | 497 | var contextHook = result['__haiku_hook'] || result 498 | 499 | if (typeof contextHook !== 'object' && typeof contextHook != 'function') 500 | throw new Error('Internal error. Unable to determine the context hook of the module ' + request + '.') 501 | 502 | if (!NativeModule.getCached(request)) { 503 | 504 | // Native modules have their identity set in NativeModule.compile. 505 | // This code path is for userland JavaScript modules only. 506 | 507 | // The module object itself is created in the main V8 context, while all its properties are created in 508 | // a separate (user) V8 context (since module._contextLoad was set to true above). One of these 509 | // properties is __haiku_hook retrieved above (its existence is enforced in module.prototype._compile above). 510 | 511 | // Propagate the identity of the handler code that started this 'require episode' 512 | // to the V8 context in which the module code had been created. 513 | 514 | var currentContextData = haiku_extensions.getCurrentContextData() 515 | 516 | if (typeof currentContextData !== 'string') 517 | throw new Error('Unable to obtain current context data to enforce it on module ' + request + '.'); 518 | 519 | haiku_extensions.setContextDataOn(contextHook, currentContextData) 520 | } 521 | 522 | // Create sandbox wrapper around the module in the module's V8 creation context 523 | 524 | if (moduleSandbox[request]) 525 | result = haiku_extensions.runInObjectContext( 526 | contextHook, 527 | function () { 528 | return createObjectSandbox(moduleSandbox[request], result) 529 | } 530 | ) 531 | 532 | // From now on, all functions defined in the module can be attributted to a particular handler invocation 533 | // by looking at a function's V8 creation context's context data using haiku_extensions.getContextDataOf(func) 534 | 535 | return result 536 | } 537 | finally { 538 | if (enteredRequireEpisode) 539 | inRequireEpisode = false 540 | } 541 | } 542 | } 543 | 544 | exports.createSandbox = createSandbox; 545 | exports.wrapFunction = wrapFunction; 546 | exports.enterModuleSandbox = enterModuleSandbox; -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | var NativeModule; 2 | 3 | // Hack to get a reference to node's internal NativeModule 4 | // Courtesy of Brandon Benvie, https://github.com/Benvie/Node.js-Ultra-REPL/blob/master/lib/ScopedModule.js 5 | (function(){ 6 | // intercept NativeModule.require's call to process.moduleLoadList.push 7 | process.moduleLoadList.push = function() { 8 | // `NativeModule.require('native_module')` returns NativeModule 9 | NativeModule = arguments.callee.caller('native_module'); 10 | 11 | // delete the interceptor and forward normal functionality 12 | delete process.moduleLoadList.push; 13 | return Array.prototype.push.apply(process.moduleLoadList, arguments); 14 | } 15 | // force one module resolution 16 | require('url'); 17 | })(); 18 | 19 | var http = require('http') 20 | , https = require('https') 21 | , url = require('url') 22 | , vm = require('vm') 23 | , cluster = require('cluster') 24 | , util = require('util') 25 | , haikuConsole = require('./haikuConsole.js') 26 | , sandbox = require('./sandbox.js') 27 | , haiku_extensions = require('./build/Release/haiku_extensions.node') 28 | 29 | var shutdown 30 | , shutdownInProgress = false 31 | , requestCount = 0 32 | , argv 33 | , scriptCount = 0 34 | 35 | process.on('message', function (msg) { 36 | process.send({ response: msg.challange }); 37 | }) 38 | .on('uncaughtException', function (err) { 39 | log('Entering shutdown mode after an uncaught exception: ' 40 | + (err.message || err) + (err.stack ? '\n' + err.stack : '')); 41 | initiateShutdown(); 42 | }); 43 | 44 | function log(thing) { 45 | console.log(process.pid + ': ' + thing); 46 | } 47 | 48 | function shutdownNext() { 49 | if (shutdown) { 50 | clearTimeout(shutdown); 51 | shutdown = undefined; 52 | } 53 | 54 | process.nextTick(function() { 55 | log('Recycling self. Active connections: TCP: ' + httpServer.connections + ', TLS: ' + httpsServer.connections); 56 | process.exit(); 57 | }); 58 | } 59 | 60 | // raised by HTTP or HTTPS server when one of the client connections closes 61 | function onConnectionClose() { 62 | if (shutdownInProgress && 0 === (httpServer.connections + httpsServer.connections)) 63 | shutdownNext() 64 | } 65 | 66 | function initiateShutdown() { 67 | if (!shutdownInProgress) { 68 | 69 | // stop accepting new requests 70 | 71 | httpServer.close(); 72 | httpsServer.close(); 73 | 74 | shutdownInProgress = true; 75 | 76 | if (0 === (httpServer.connections + httpsServer.connections)) { 77 | // there are no active connections - shut down now 78 | 79 | shutdownNext(); 80 | } 81 | else { 82 | // Shut down when all active connections close (see onConnectionClose above) 83 | // or when the graceful shutdown timeout expires, whichever comes first. 84 | // Graceful shutdown timeout is twice the handler processing timeout. 85 | 86 | shutdown = setTimeout(shutdownNext, argv.t * 2); 87 | } 88 | } 89 | } 90 | 91 | function onRequestFinished(context) { 92 | if (!context.finished) { 93 | context.finished = true; 94 | context.req.socket.end(); // force buffers to be be flushed 95 | } 96 | } 97 | 98 | function haikuError(context, status, error) { 99 | log(new Date() + ' Status: ' + status + ', Request URL: ' + context.req.url + ', Error: ' + error); 100 | try { 101 | context.req.resume(); 102 | context.res.writeHead(status); 103 | if (error && 'HEAD' !== context.req.method) 104 | context.res.end((typeof error === 'string' ? error : JSON.stringify(error)) + '\n'); 105 | else 106 | context.res.end(); 107 | } 108 | catch (e) { 109 | // empty 110 | } 111 | onRequestFinished(context); 112 | } 113 | 114 | function limitExecutionTime(context) { 115 | 116 | // setup timeout for request processing 117 | 118 | context.timeout = setTimeout(function () { 119 | delete context.timeout; 120 | haikuError(context, 500, 'Handler ' + context.handlerName + ' did not complete within the time limit of ' + argv.t + 'ms'); 121 | onRequestFinished(context); 122 | }, argv.t); // handler processing timeout 123 | 124 | // intercept end of response to cancel the timeout timer and 125 | // speed up shutdown if one is in progress 126 | 127 | context.res.end = sandbox.wrapFunction(context.res, 'end', function () { 128 | var result = arguments[--arguments.length].apply(this, arguments); 129 | if (context.timeout) { 130 | clearTimeout(context.timeout); 131 | delete context.timeout; 132 | onRequestFinished(context); 133 | } 134 | return result; 135 | }); 136 | } 137 | 138 | function executeHandler(context) { 139 | log(new Date() + ' executing ' + context.handlerName); 140 | 141 | // limit execution time of the handler to the preconfigured value 142 | 143 | limitExecutionTime(context); 144 | 145 | // expose rigged console through sandbox 146 | 147 | var sandboxAddons = { 148 | console: haikuConsole.createConsole(context, argv.l, argv.d) 149 | } 150 | 151 | // evaluate handler code in strict mode to prevent stack walking from untrusted code 152 | 153 | context.handler = "'use strict';" + context.handler 154 | 155 | // calculate the script name which will identify user code 156 | 157 | var scriptName = (scriptCount++) + '#' + context.handlerName 158 | 159 | context.req.resume(); 160 | var inUserCode = false; 161 | try { 162 | // create a new V8 context for executing the handler 163 | 164 | var v8ctx = vm.createContext(sandbox.createSandbox(context, sandboxAddons)) 165 | 166 | // first associate a script id with that V8 context so that later we can attribute JS callbacks 167 | // to a particular haiku-handler 168 | 169 | var contextHook = vm.runInContext("(function () { return {}; })();", v8ctx, scriptName) 170 | haiku_extensions.setContextDataOn(contextHook, scriptName) 171 | 172 | // notify the watchdog we are entering user code 173 | 174 | haiku_extensions.enterUserCode(contextHook) 175 | inUserCode = true 176 | 177 | // finally execute the actual handler code 178 | 179 | vm.runInContext(context.handler, v8ctx, scriptName) 180 | } 181 | catch (e) { 182 | // notify the watchdog we have left user code 183 | 184 | if (inUserCode) { 185 | haiku_extensions.leaveUserCode() 186 | inUserCode = false 187 | } 188 | 189 | haikuError(context, 500, 'Handler ' + context.handlerName + ' generated an exception at runtime: ' 190 | + (e.message || e) + (e.stack ? '\n' + e.stack : '')); 191 | } 192 | finally { 193 | // notify the watchdog we have left user code 194 | 195 | if (inUserCode) 196 | haiku_extensions.leaveUserCode() 197 | } 198 | } 199 | 200 | function resolveHandler(context) { 201 | if (!context.handlerName) 202 | return haikuError(context, 400, 203 | 'The x-haiku-handler HTTP request header or query paramater must specify the URL of the scriptlet to run.'); 204 | 205 | try { 206 | context.handlerUrl = url.parse(context.handlerName); 207 | } 208 | catch (e) { 209 | return haikuError(context, 400, 'The x-haiku-handler parameter must be a valid URL that resolves to a JavaScript scriptlet.'); 210 | } 211 | 212 | var engine; 213 | if (context.handlerUrl.protocol === 'http:') { 214 | engine = http; 215 | context.handlerUrl.port = context.handlerUrl.port || 80; 216 | } 217 | else if (context.handlerUrl.protocol === 'https:') { 218 | engine = https; 219 | context.handlerUrl.port = context.handlerUrl.port || 443; 220 | } 221 | else 222 | return haikuError(context, 400, 'The x-haiku-handler parameter specifies unsupported protocol. Only http and https are supported.'); 223 | 224 | var handlerRequest; 225 | var processResponse = function(res) { 226 | context.handler = ''; 227 | var length = 0; 228 | res.on('data', function(chunk) { 229 | length += chunk.length; 230 | if (length > argv.i) { 231 | handlerRequest.abort(); 232 | return haikuError(context, 400, 'The size of the handler exceeded the quota of ' + argv.i + ' bytes.'); 233 | } 234 | context.handler += chunk; 235 | }) 236 | .on('end', function() { 237 | if (res.statusCode === 200) 238 | executeHandler(context); 239 | else if (res.statusCode === 302 && context.redirect < 3) { 240 | context.handlerName = res.headers['location']; 241 | context.redirect++; 242 | resolveHandler(context); 243 | } 244 | else 245 | return haikuError(context, 400, 'HTTP error when obtaining handler code from ' + context.handlerName + ': ' + res.statusCode); 246 | }); 247 | } 248 | 249 | var processError = function(error) { 250 | haikuError(context, 400, 'Unable to obtain HTTP handler code from ' + context.handlerName + ': ' + error); 251 | } 252 | 253 | if (argv.proxyHost) { 254 | // HTTPS or HTTP request through HTTP proxy 255 | http.request({ // establishing a tunnel 256 | host: argv.proxyHost, 257 | port: argv.proxyPort, 258 | method: 'CONNECT', 259 | path: context.handlerUrl.hostname + ':' + context.handlerUrl.port 260 | }).on('connect', function(pres, socket, head) { 261 | if (pres.statusCode !== 200) 262 | return haikuError(context, 400, 'Unable to connect to the host ' + context.host); 263 | else 264 | handlerRequest = engine.get({ 265 | host: context.handlerUrl.hostname, 266 | port: context.handlerUrl.port, 267 | path: context.handlerUrl.path, 268 | socket: socket, // using a tunnel 269 | agent: false // cannot use a default agent 270 | }, processResponse).on('error', processError); 271 | }).on('error', processError).end(); 272 | } 273 | else // no proxy 274 | handlerRequest = engine.get({ 275 | host: context.handlerUrl.hostname, 276 | port: context.handlerUrl.port, 277 | path: context.handlerUrl.path 278 | }, processResponse).on('error', processError); 279 | } 280 | 281 | function getHaikuParam(context, name, defaultValue) { 282 | return context.req.headers[name] || context.reqUrl.query[name] || defaultValue; 283 | } 284 | 285 | function processRequest(req, res) { 286 | 287 | if (req.url === '/favicon.ico') 288 | return haikuError({ req: req, res: res}, 404); 289 | 290 | if (!shutdownInProgress && argv.r > 0 && ++requestCount >= argv.r) { 291 | log('Entering shutdown mode after reaching request quota. Current active connections: TCP: ' 292 | + httpServer.connections + ', TLS: ' + httpsServer.connections); 293 | initiateShutdown(); 294 | } 295 | 296 | req.pause(); 297 | 298 | var context = { 299 | req: req, 300 | res: res, 301 | redirect: 0, 302 | reqUrl: url.parse(req.url, true) 303 | } 304 | context.handlerName = getHaikuParam(context, 'x-haiku-handler'); 305 | context.console = getHaikuParam(context, 'x-haiku-console', 'none'); 306 | 307 | resolveHandler(context); 308 | } 309 | 310 | exports.main = function(args) { 311 | argv = args; 312 | 313 | // enter module sanbox - from now on all module reustes in this process will 314 | // be subject to sandboxing 315 | 316 | httpServer = http.createServer(processRequest) 317 | .on('connection', function(socket) { 318 | socket.on('close', onConnectionClose) 319 | }) 320 | .listen(argv.p); 321 | 322 | httpsServer = https.createServer({ cert: argv.cert, key: argv.key }, processRequest) 323 | .on('connection', function(socket) { 324 | socket.on('close', onConnectionClose) 325 | }) 326 | .listen(argv.s); 327 | 328 | haiku_extensions.setContextDataOn(processRequest, 'MainContext') 329 | 330 | sandbox.enterModuleSandbox(NativeModule); 331 | } 332 | -------------------------------------------------------------------------------- /test/000_prerequisite.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , mongodb = require('mongodb') 3 | , assert = require('assert') 4 | 5 | describe('000_prerequisities.js:', function () { 6 | describe('haiku-http service', function () { 7 | it('is running at http://localhost. Please start the haiku-http service with "sudo node src/haiku-http.js".', function (done) { 8 | request('http://localhost', function (err, res, body) { 9 | assert.ifError(err) 10 | assert.equal(res.statusCode, 400) 11 | done() 12 | }) 13 | }) 14 | }) 15 | 16 | describe('haiku-http script service', function () { 17 | it('is running at http://localhost:8000. Please start the haiku-http script service with "node samples/server.js".', function (done) { 18 | request('http://localhost:8000/hello', function (err, res, body) { 19 | assert.ifError(err) 20 | assert.equal(res.statusCode, 200) 21 | done() 22 | }) 23 | }) 24 | }) 25 | 26 | describe('direct (non-proxied) internet connection', function () { 27 | it('is available', function (done) { 28 | request('http://www.google.com', function (err, res, body) { 29 | assert.ifError(err) 30 | assert.equal(res.statusCode, 200) 31 | done() 32 | }) 33 | }) 34 | }) 35 | 36 | describe('MonghDB database at mongodb://arr:arr@staff.mongohq.com:10024/arr', function () { 37 | it('is available', function (done) { 38 | mongodb.connect('mongodb://arr:arr@staff.mongohq.com:10024/arr', function (err, db) { 39 | assert.ifError(err) 40 | db.close() 41 | done() 42 | }) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/200_samples_hello.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , assert = require('assert') 3 | 4 | describe('200_samples_hello.js:', function () { 5 | it('http://localhost?x-haiku-handler=http://localhost:8000/hello returns Hello, world!', function (done) { 6 | request('http://localhost?x-haiku-handler=http://localhost:8000/hello', function (err, res, body) { 7 | assert.ifError(err) 8 | assert.equal(res.statusCode, 200) 9 | assert.equal(body, 'Hello, world!\n') 10 | done() 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/201_samples_delayed-hello.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , assert = require('assert') 3 | 4 | describe('201_samples_delayed-hello.js:', function () { 5 | it('http://localhost?x-haiku-handler=http://localhost:8000/delayed-hello returns Hello, world in two lines', function (done) { 6 | request('http://localhost?x-haiku-handler=http://localhost:8000/delayed-hello', function (err, res, body) { 7 | assert.ifError(err) 8 | assert.equal(res.statusCode, 200) 9 | assert.ok(body.indexOf('Hello') !== -1) 10 | assert.ok(body.indexOf('world') !== -1) 11 | done() 12 | }) 13 | }) 14 | }) -------------------------------------------------------------------------------- /test/202_samples_mongo.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , assert = require('assert') 3 | 4 | describe('202_samples_mongo.js:', function () { 5 | it('http://localhost?x-haiku-handler=http://localhost:8000/mongo returns two documents', function (done) { 6 | request('http://localhost?x-haiku-handler=http://localhost:8000/mongo', function (err, res, body) { 7 | assert.ifError(err) 8 | assert.equal(res.statusCode, 200) 9 | assert.ok(body.indexOf('app1.com') !== -1) 10 | assert.ok(body.indexOf('app2.com') !== -1) 11 | done() 12 | }) 13 | }) 14 | 15 | it('http://localhost?x-haiku-handler=http://localhost:8000/mongo&host=app1.com returns one document for app1.com', function (done) { 16 | request('http://localhost?x-haiku-handler=http://localhost:8000/mongo&host=app1.com', function (err, res, body) { 17 | assert.ifError(err) 18 | assert.equal(res.statusCode, 200) 19 | assert.ok(body.indexOf('app1.com') !== -1) 20 | assert.ok(body.indexOf('app2.com') === -1) 21 | done() 22 | }) 23 | }) 24 | 25 | it('http://localhost?x-haiku-handler=http://localhost:8000/mongo&host=app2.com returns one document for app2.com', function (done) { 26 | request('http://localhost?x-haiku-handler=http://localhost:8000/mongo&host=app2.com', function (err, res, body) { 27 | assert.ifError(err) 28 | assert.equal(res.statusCode, 200) 29 | assert.ok(body.indexOf('app1.com') === -1) 30 | assert.ok(body.indexOf('app2.com') !== -1) 31 | done() 32 | }) 33 | }) 34 | 35 | it('http://localhost?x-haiku-handler=http://localhost:8000/mongo&host=foobar returns no documents', function (done) { 36 | request('http://localhost?x-haiku-handler=http://localhost:8000/mongo&host=foobar', function (err, res, body) { 37 | assert.ifError(err) 38 | assert.equal(res.statusCode, 200) 39 | assert.ok(body.indexOf('app1.com') === -1) 40 | assert.ok(body.indexOf('app2.com') === -1) 41 | done() 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/203_samples_https.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , assert = require('assert') 3 | 4 | describe('203_samples_https.js:', function () { 5 | it('http://localhost?x-haiku-handler=http://localhost:8000/https returns GitHub home page', function (done) { 6 | request('http://localhost?x-haiku-handler=http://localhost:8000/https', function (err, res, body) { 7 | assert.ifError(err) 8 | assert.equal(res.statusCode, 200) 9 | assert.ok(body.indexOf('GitHub') !== -1) 10 | done() 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/204_samples_request.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , assert = require('assert') 3 | 4 | describe('204_samples_request.js:', function () { 5 | it('http://localhost?x-haiku-handler=http://localhost:8000/request returns results for "the" word', function (done) { 6 | request('http://localhost?x-haiku-handler=http://localhost:8000/request', function (err, res, body) { 7 | assert.ifError(err) 8 | assert.equal(res.statusCode, 200) 9 | assert.ok(body.indexOf('"the"') !== -1) 10 | done() 11 | }) 12 | }) 13 | 14 | it('http://localhost?x-haiku-handler=http://localhost:8000/request&word=economy returns results for "economy" word', function (done) { 15 | request('http://localhost?x-haiku-handler=http://localhost:8000/request&word=economy', function (err, res, body) { 16 | assert.ifError(err) 17 | assert.equal(res.statusCode, 200) 18 | assert.ok(body.indexOf('"economy"') !== -1) 19 | done() 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/205_samples_console.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , assert = require('assert') 3 | 4 | describe('205_samples_console.js:', function () { 5 | it('http://localhost?x-haiku-handler=http://localhost:8000/console returns Hello, World!', function (done) { 6 | request('http://localhost?x-haiku-handler=http://localhost:8000/console', function (err, res, body) { 7 | assert.ifError(err) 8 | assert.equal(res.statusCode, 200) 9 | assert.equal(body, 'Hello, world!\n') 10 | done() 11 | }) 12 | }) 13 | 14 | it('http://localhost?x-haiku-handler=http://localhost:8000/console&x-haiku-console=body returns console output in the body', function (done) { 15 | request('http://localhost?x-haiku-handler=http://localhost:8000/console&x-haiku-console=body', function (err, res, body) { 16 | assert.ifError(err) 17 | assert.equal(res.statusCode, 200) 18 | assert.equal(body, 'Before writeHead\nAfter writeHead and before write\nAfter write and before end\n') 19 | done() 20 | }) 21 | }) 22 | 23 | it('http://localhost?x-haiku-handler=http://localhost:8000/console&x-haiku-console=header returns console output in the header and Hello, world! in the boxy', function (done) { 24 | request('http://localhost?x-haiku-handler=http://localhost:8000/console&x-haiku-console=header', function (err, res, body) { 25 | assert.ifError(err) 26 | assert.equal(res.statusCode, 200) 27 | assert.equal(res.headers['x-haiku-console'], 'Before writeHead%0A') 28 | assert.equal(body, 'Hello, world!\n') 29 | done() 30 | }) 31 | }) 32 | 33 | it('http://localhost?x-haiku-handler=http://localhost:8000/console&x-haiku-console=trailer returns console output in the trailer and Hello, world! in the boxy', function (done) { 34 | request('http://localhost?x-haiku-handler=http://localhost:8000/console&x-haiku-console=trailer', function (err, res, body) { 35 | assert.ifError(err) 36 | assert.equal(res.statusCode, 200) 37 | assert.equal(res.headers['trailer'], 'x-haiku-console') 38 | assert.equal(res.trailers['x-haiku-console'], 'Before writeHead%0AAfter writeHead and before write%0AAfter write and before end%0A') 39 | assert.equal(body, 'Hello, world!\n') 40 | done() 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/400_entrypoint.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , assert = require('assert') 3 | 4 | describe('400_entrypoint.js:', function () { 5 | it('http://localhost?x-haiku-handler=http://localhost:8000/tests/entrypoint_setInterval is sandboxed', function (done) { 6 | request('http://localhost?x-haiku-handler=http://localhost:8000/tests/entrypoint_setInterval', function (err, res, body) { 7 | assert.ifError(err) 8 | assert.equal(res.statusCode, 200) 9 | var result = JSON.parse(body) 10 | assert.equal(result[0], result[1]) 11 | assert.ok(0 < result[0].indexOf('#')) 12 | assert.equal(result[0].substring(result[0].indexOf('#') + 1), 'http://localhost:8000/tests/entrypoint_setInterval') 13 | done() 14 | }) 15 | }) 16 | 17 | it('http://localhost?x-haiku-handler=http://localhost:8000/tests/entrypoint_setTimeout is sandboxed', function (done) { 18 | request('http://localhost?x-haiku-handler=http://localhost:8000/tests/entrypoint_setTimeout', function (err, res, body) { 19 | assert.ifError(err) 20 | assert.equal(res.statusCode, 200) 21 | var result = JSON.parse(body) 22 | assert.equal(result[0], result[1]) 23 | assert.ok(0 < result[0].indexOf('#')) 24 | assert.equal(result[0].substring(result[0].indexOf('#') + 1), 'http://localhost:8000/tests/entrypoint_setTimeout') 25 | done() 26 | }) 27 | }) 28 | 29 | it('http://localhost?x-haiku-handler=http://localhost:8000/tests/entrypoint_nextTick is sandboxed', function (done) { 30 | request('http://localhost?x-haiku-handler=http://localhost:8000/tests/entrypoint_nextTick', function (err, res, body) { 31 | assert.ifError(err) 32 | assert.equal(res.statusCode, 200) 33 | var result = JSON.parse(body) 34 | assert.equal(result[0], result[1]) 35 | assert.ok(0 < result[0].indexOf('#')) 36 | assert.equal(result[0].substring(result[0].indexOf('#') + 1), 'http://localhost:8000/tests/entrypoint_nextTick') 37 | done() 38 | }) 39 | }) 40 | 41 | it('http://localhost?x-haiku-handler=http://localhost:8000/tests/entrypoint_EventEmitter is sandboxed (using MongoDB)', function (done) { 42 | request('http://localhost?x-haiku-handler=http://localhost:8000/tests/entrypoint_EventEmitter', function (err, res, body) { 43 | assert.ifError(err) 44 | assert.equal(res.statusCode, 200) 45 | var result = JSON.parse(body) 46 | for (var i = 1; i < result.length; i++) 47 | assert.equal(result[0], result[i]) 48 | assert.ok(0 < result[0].indexOf('#')) 49 | assert.equal(result[0].substring(result[0].indexOf('#') + 1), 'http://localhost:8000/tests/entrypoint_EventEmitter') 50 | done() 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /wscript: -------------------------------------------------------------------------------- 1 | srcdir = '.' 2 | blddir = 'src/build' 3 | VERSION = '0.0.1' 4 | 5 | def set_options(opt): 6 | opt.tool_options('compiler_cxx') 7 | 8 | def configure(conf): 9 | conf.check_tool('compiler_cxx') 10 | conf.check_tool('node_addon') 11 | 12 | def build(bld): 13 | obj = bld.new_task_gen('cxx', 'shlib', 'node_addon') 14 | obj.target = 'haiku_extensions' 15 | obj.source = 'src/haiku_extensions.cc' --------------------------------------------------------------------------------