")
16 | end
17 | end
18 |
19 | def get_attribute_value item, attribute
20 | obj = item
21 | properties = attribute.split('.')
22 |
23 | properties.each do |prop|
24 | if obj.respond_to? :has_key?
25 | obj = obj[prop]
26 | else
27 | obj = obj.send(prop.to_sym)
28 | end
29 | end
30 |
31 | return obj
32 | end
33 |
34 | def render(context)
35 | list = get_attribute_value context, @list
36 |
37 | return '' unless list.length
38 |
39 | old_result = nil
40 | if context.has_key? @group_name
41 | old_result = context[@group_name]
42 | end
43 |
44 | old_grouper = nil
45 | if context.has_key? 'grouper'
46 | old_grouper = context['grouper']
47 | end
48 |
49 | prev_attribute = get_attribute_value list.first, @attribute
50 | current_group = []
51 |
52 | rendered = []
53 |
54 | list.each do |item|
55 | current_attribute = get_attribute_value item, @attribute
56 | if current_attribute != prev_attribute
57 | context[@group_name] = current_group
58 | context['grouper'] = prev_attribute
59 | rendered << super(context)
60 | current_group = []
61 | end
62 |
63 | current_group.push item
64 | prev_attribute = current_attribute
65 | end
66 | context[@group_name] = current_group
67 | context['grouper'] = prev_attribute
68 | rendered << super(context)
69 |
70 |
71 | context[@group_name] = old_result
72 | context['grouper'] = old_grouper
73 |
74 | return rendered
75 | end
76 | end
77 | end
78 |
79 | Liquid::Template.register_tag('regroup', BTorg::RegroupBlock)
80 |
--------------------------------------------------------------------------------
/test/test-request.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var TestSuite = require('../dependencies/node-async-testing/async_testing').TestSuite;
3 |
4 | var BomberRequest = require('../lib/request').Request;
5 | var MockRequest = require('./mocks/request').MockRequest;
6 |
7 | (new TestSuite('Request Tests'))
8 | .setup(function() {
9 | this.mr = new MockRequest('POST', '/');
10 | this.br = new BomberRequest(this.mr, {"href": "/", "pathname": "/"}, {});
11 | })
12 | .runTests({
13 | "test simple": function(test) {
14 | this.assert.equal(this.mr.method, this.br.method);
15 | },
16 | "test load data": function(test) {
17 | var sent = 'foo=bar&baz%5Bquux%5D=asdf&baz%5Boof%5D=rab&boo%5B%5D=1';
18 | var parsed = {
19 | "foo": "bar",
20 | "baz": {
21 | "quux": "asdf",
22 | "oof": "rab"
23 | },
24 | "boo": [
25 | 1
26 | ]
27 | };
28 |
29 | var p = test.br.loadData();
30 | p.addCallback(function(data) {
31 | test.assert.deepEqual(parsed, data);
32 | });
33 |
34 | test.mr.emit('body',sent);
35 | test.mr.emit('complete');
36 | },
37 | "test load data -- no parse": function(test) {
38 | var sent = 'foo=bar&baz%5Bquux%5D=asdf&baz%5Boof%5D=rab&boo%5B%5D=1';
39 |
40 | var p = test.br.loadData(false);
41 | p.addCallback(function(data) {
42 | test.assert.equal(sent, data);
43 | });
44 |
45 | test.mr.emit('body',sent);
46 | test.mr.emit('complete');
47 | },
48 | "test buffers": function(test) {
49 | var dataLoaded = false;
50 |
51 | var first = 'one',
52 | second = 'two';
53 |
54 | var p = test.br.loadData(false);
55 | p.addCallback(function(data) {
56 | test.assert.equal(first+second, data);
57 | dataLoaded = true;
58 | });
59 |
60 | test.assert.ok(!dataLoaded);
61 | test.mr.emit('body',first);
62 | test.assert.ok(!dataLoaded);
63 | test.mr.emit('body',second);
64 | test.assert.ok(!dataLoaded);
65 | test.mr.emit('complete');
66 | test.assert.ok(dataLoaded);
67 | },
68 | });
69 |
--------------------------------------------------------------------------------
/test/fixtures/testApp/views/cookie-tests.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 |
3 | exports.set = function(request, response) {
4 | for(var key in request.params) {
5 | request.cookies.set(key, request.params[key]);
6 | }
7 |
8 | var read = [];
9 | for( var key in request.params ) {
10 | read.push(request.cookies.get(key));
11 | }
12 |
13 | return read.join(',');
14 | };
15 |
16 | exports.read = function(request, response) {
17 | var read = [];
18 | var def = request.params['_default'];
19 | for(var key in request.params) {
20 | if( key == '_default' ) {
21 | continue;
22 | }
23 | var val = request.cookies.get(key,def);
24 | if( val === null ) {
25 | continue;
26 | }
27 | read.push(val);
28 | }
29 |
30 | return read.join(',');
31 | }
32 |
33 | exports.setSecure = function(request, response) {
34 | var count = 0;
35 | for(var key in request.params) {
36 | request.cookies.setSecure(key, request.params[key]);
37 | count++;
38 | }
39 |
40 | return ''+count;
41 | };
42 |
43 | exports.readSecure = function(request, response) {
44 | var read = [];
45 | var def = request.params['_default'];
46 | for(var key in request.params) {
47 | if( key == '_default' ) {
48 | continue;
49 | }
50 | read.push(request.cookies.getSecure(key,def));
51 | }
52 |
53 | return read.join(',');
54 | }
55 |
56 | exports.unset = function(request, response) {
57 | for(var key in request.params) {
58 | request.cookies.unset(key);
59 | }
60 |
61 | var read = [];
62 | for( var key in request.params ) {
63 | read.push(request.cookies.get(key));
64 | }
65 |
66 | return read.join(',');
67 | };
68 |
69 | exports.reset = function(request, response) {
70 | request.cookies.reset();
71 | return '';
72 | };
73 |
74 | exports.keys = function(request, response) {
75 | return request.cookies.keys().join(',');
76 | };
77 |
78 | exports.exists = function(request, response) {
79 | var existence = [];
80 | for(var key in request.params) {
81 | if(request.cookies.get(key)) {
82 | existence.push(1);
83 | }
84 | else {
85 | existence.push(0);
86 | }
87 | }
88 |
89 | return existence.join(',');
90 | };
91 |
--------------------------------------------------------------------------------
/dependencies/node-async-testing/examples/suite.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var TestSuite = require('../async_testing').TestSuite;
3 |
4 | (new TestSuite('My Second Test Suite'))
5 | .runTests({
6 | "this does something": function(test) {
7 | test.assert.ok(true);
8 | test.finish();
9 | },
10 | "this doesn't fail": function(test) {
11 | test.assert.ok(true);
12 | setTimeout(function() {
13 | test.assert.ok(true);
14 | test.finish();
15 | }, 1000);
16 | },
17 | "this does something else": function(test) {
18 | test.assert.ok(true);
19 | test.assert.ok(true);
20 | test.finish();
21 | },
22 | });
23 |
24 | (new TestSuite('My First Test Suite'))
25 | .runTests({
26 | "this does something": function(test) {
27 | test.assert.ok(true);
28 | test.finish();
29 | },
30 | "this fails": function(test) {
31 | setTimeout(function() {
32 | test.assert.ok(false);
33 | test.finish();
34 | }, 1000);
35 | },
36 | "this does something else": function(test) {
37 | test.assert.ok(true);
38 | test.assert.ok(true);
39 | test.finish();
40 | },
41 | "more": function(test) {
42 | test.assert.ok(true);
43 | test.finish();
44 | },
45 | "throws": function(test) {
46 | test.assert.throws(function() {
47 | throw new Error();
48 | });
49 | test.finish();
50 | },
51 | "expected assertions": function(test) {
52 | test.numAssertionsExpected = 1;
53 | test.assert.throws(function() {
54 | throw new Error();
55 | });
56 | test.finish();
57 | },
58 | });
59 |
60 | (new TestSuite("Setup"))
61 | .setup(function(test) {
62 | test.foo = 'bar';
63 | })
64 | .runTests({
65 | "foo equals bar": function(test) {
66 | test.assert.equal('bar', test.foo);
67 | }
68 | });
69 |
70 | var count = 0;
71 | var ts = new TestSuite('Wait');
72 | ts.wait = true;
73 | ts.runTests({
74 | "count equal 0": function(test) {
75 | test.assert.equal(0, count);
76 | setTimeout(function() {
77 | count++;
78 | test.finish();
79 | }, 50);
80 | },
81 | "count equal 1": function(test) {
82 | test.assert.equal(1, count);
83 | test.finish();
84 | }
85 | });
86 |
87 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | Bomber is a node.js web framework inspired by Rails, Django and anything else out there that has caught our eye.
2 |
3 | Main website and documentation: http://bomber.obtdev.com/
4 |
5 | Warning! Right now the API is very much in a state of flux. We are still experimenting with the best way to make Bomber both intuitive and powerful. Expect things to change a lot for the forseeable future.
6 |
7 | Getting up and running
8 | ----------------------
9 |
10 | The source code for Bomber is [located on GitHub][bomber-src]. To check it out, run the following command:
11 |
12 | git checkout git://github.com/obt/bomberjs.git
13 |
14 | The easiest way to try it out is to `cd` into the example project and run the following command to start the server (assuming you have [Node] installed and in your path):
15 |
16 | cd bomberjs/exampleProject
17 | ../bomber.js start-server
18 |
19 |
20 | Brief summary
21 | -------------
22 |
23 | Bomber is centered around the idea of 'apps'. Apps are just a bunch of functionality wrapped up into a folder.
24 |
25 | Here is what an app folder structure could look like:
26 |
27 | app-name/
28 | ./routes.js
29 | ./views/
30 | ./view-name.js
31 |
32 | Here is an example `routes.js` file:
33 |
34 | var Router = require('bomber/lib/router').Router;
35 | var r = new Router();
36 |
37 | r.add('/:view/:action/:id');
38 | r.add('/:view/:action');
39 |
40 | exports.router = r;
41 |
42 | Here is an example view file:
43 |
44 | exports.index = function(request, response) {
45 | return "index action";
46 | };
47 | exports.show = function(request, response) {
48 | if( request.format == 'json' ) {
49 | return {a: 1, b: 'two', c: { value: 'three'}};
50 | }
51 | else {
52 | return "show action";
53 | }
54 | };
55 |
56 | Participating
57 | -------------
58 |
59 | We are open to new ideas and feedback, so if you have any thoughts on ways to make Bomber better, or find any bugs, please feel free to [participate in the Google Group](http://groups.google.com/group/bomberjs).
60 |
61 | Relevant Reading
62 | ----------------
63 |
64 | + [Blog post announcing the motivation](http://benjaminthomas.org/2009-11-20/designing-a-web-framework.html)
65 | + [Blog post discussing the design of the routing](http://benjaminthomas.org/2009-11-24/bomber-routing.html)
66 | + [Blog post discussing the design of the actions](http://benjaminthomas.org/2009-11-29/bomber-actions.html)
67 |
68 | [bomber-src]: http://github.com/obt/bomberjs
69 | [Node]: http://nodejs.org/
70 |
71 |
--------------------------------------------------------------------------------
/test/test-http_responses.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var TestSuite = require('../dependencies/node-async-testing/async_testing').TestSuite;
3 |
4 | var responses = require('../lib/http_responses');
5 | var BomberResponse = require('../lib/response').Response;
6 | var MockResponse = require('./mocks/response').MockResponse;
7 |
8 | (new TestSuite('Default HTTPResponse tests'))
9 | .setup(function(test) {
10 | test.mr = new MockResponse();
11 | test.br = new BomberResponse(test.mr);
12 | })
13 | .runTests({
14 | "test a default response": function(test) {
15 | var r = new responses.HTTP200OK();
16 | r.respond(test.br);
17 |
18 | test.assert.equal(200, test.mr.status);
19 | test.assert.equal('HTTP200OK', test.mr.bodyText);
20 | test.assert.ok(test.mr.finished);
21 | },
22 | "test can set body in constructor": function(test) {
23 | var r = new responses.HTTP200OK('body');
24 |
25 | r.respond(test.br);
26 | test.assert.equal('body', test.mr.bodyText);
27 | },
28 | "test can set body explicitly": function(test) {
29 | var r = new responses.HTTP200OK();
30 | r.body = 'body';
31 |
32 | r.respond(test.br);
33 | test.assert.equal('body', test.mr.bodyText);
34 | },
35 | "test can set Content-Type": function(test) {
36 | var r = new responses.HTTP200OK();
37 | r.mimeType = 'mimetype';
38 | r.respond(test.br);
39 |
40 | test.assert.equal('mimetype', test.mr.headers['Content-Type']);
41 | },
42 | "test can set status": function(test) {
43 | var r = new responses.HTTP200OK();
44 | r.status = 200;
45 | r.respond(test.br);
46 |
47 | test.assert.equal(200, test.mr.status);
48 | },
49 | });
50 |
51 | (new TestSuite('HTTPResponse Redirect tests'))
52 | .setup(function() {
53 | this.mr = new MockResponse();
54 | this.br = new BomberResponse(this.mr);
55 | })
56 | .runTests({
57 | "test redirect with no status": function(test) {
58 | var r = new responses.redirect('url');
59 | r.respond(test.br);
60 |
61 | test.assert.equal(301, test.mr.status);
62 | test.assert.equal('url', test.mr.headers.Location);
63 | test.assert.equal('', test.mr.bodyText);
64 | test.assert.ok(test.mr.finished);
65 | },
66 | "test redirect with status": function(test) {
67 | var r = new responses.redirect('url', 1);
68 | r.respond(test.br);
69 |
70 | test.assert.equal(1, test.mr.status);
71 | test.assert.equal('url', test.mr.headers.Location);
72 | test.assert.equal('', test.mr.bodyText);
73 | test.assert.ok(test.mr.finished);
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/dependencies/node-httpclient/tests.js:
--------------------------------------------------------------------------------
1 | var sys = require("sys");
2 | var httpcli = require("./lib/httpclient");
3 | var testnum = 0;
4 | var client = new httpcli.httpclient();
5 |
6 | // return false to reject the certificate
7 | function verifyTLS(request) {
8 | /*
9 | sys.puts(sys.inspect(request));
10 | */
11 | return true;
12 | }
13 |
14 | // reuses one http client for all requests
15 | function runtest(url, method, data, exheaders, tlscb) {
16 | var mytestnum = ++testnum;
17 | sys.puts(mytestnum + " : " + url);
18 | var startTime = new Date();
19 | client.perform(url, method, function(result) {
20 | var tt = new Date().getTime() - startTime.getTime();
21 | sys.puts(mytestnum + " : " + result.response.status + " : " + tt);
22 | /*
23 | sys.puts(sys.inspect(result));
24 | */
25 | }, data, exheaders, tlscb);
26 | }
27 |
28 | // creates a new client for each request
29 | function runtest2(url, method, data, exheaders, tlscb) {
30 | var mytestnum = ++testnum;
31 | sys.puts(mytestnum + " : " + url);
32 | var startTime = new Date();
33 | var client = new httpcli.httpclient();
34 | client.perform(url, method, function(result) {
35 | var tt = new Date().getTime() - startTime.getTime();
36 | sys.puts(mytestnum + " : " + result.response.status + " : " + tt);
37 | /*
38 | sys.puts(sys.inspect(result));
39 | */
40 | }, data, exheaders, tlscb);
41 | }
42 |
43 | function runtests(url, foo) {
44 | foo("http://" + url, "GET", null, {"Connection" : "close"}, null);
45 | foo("https://" + url, "GET", null, {"Connection" : "close"}, verifyTLS);
46 | foo("https://" + url, "GET", null, {"Connection" : "close"});
47 | foo("http://" + url, "GET", null, {"Accept-Encoding" : "gzip", "Connection" : "close"}, null);
48 | foo("https://" + url, "GET", null, {"Accept-Encoding" : "gzip", "Connection" : "close"});
49 | foo("https://" + url, "GET", null, {"Accept-Encoding" : "gzip", "Connection" : "close"}, verifyTLS);
50 | foo("http://" + url, "GET", null, {"Connection" : "Keep-Alive"}, null);
51 | foo("https://" + url, "GET", null, {"Connection" : "Keep-Alive"});
52 | foo("https://" + url, "GET", null, {"Connection" : "Keep-Alive"}, verifyTLS);
53 | foo("http://" + url, "GET", null, {"Accept-Encoding" : "gzip", "Connection" : "Keep-Alive"}, null);
54 | foo("https://" + url, "GET", null, {"Accept-Encoding" : "gzip", "Connection" : "Keep-Alive"});
55 | foo("https://" + url, "GET", null, {"Accept-Encoding" : "gzip", "Connection" : "Keep-Alive"}, verifyTLS);
56 | }
57 |
58 | // put a domain name and path here for an address that has the features you want to test (ssl, gzip, keepalive)
59 | // be sure to exclude the protocol
60 | runtests("www.google.co.uk", runtest);
61 | runtests("www.google.co.uk", runtest2);
62 |
--------------------------------------------------------------------------------
/website/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Introducing Bomber
4 | ---
5 |
6 | Bomber is a node.js web framework inspired by Rails, Django and anything else out there that has caught our eye.
7 |
8 |
9 | Warning! Right now the API is very much in a state of flux. We are still experimenting with the best way to make Bomber both intuitive and powerful. Expect things to change a lot for the forseeable future.
10 |
11 |
12 | Getting up and running
13 | ----------------------
14 |
15 | The source code for Bomber is [located on GitHub][bomber-src]. To check it out, run the following command:
16 |
17 | {% highlight sh %}
18 | git checkout git://github.com/obt/bomberjs.git
19 | {% endhighlight %}
20 |
21 | The easiest way to try it out is to `cd` into the example project and run the following command to start the server (assuming you have [Node] installed and in your path):
22 |
23 | {% highlight sh %}
24 | cd bomberjs/exampleProject
25 | ./bomber.js start-server
26 | {% endhighlight %}
27 |
28 |
29 | Brief summary
30 | -------------
31 |
32 | Bomber is centered around the idea of 'apps'. Apps are just a bunch of functionality wrapped up into a folder.
33 |
34 | Here is what an app folder structure could look like:
35 |
36 | {% highlight text %}
37 | app-name/
38 | ./routes.js
39 | ./views/
40 | ./view-name.js
41 | {% endhighlight %}
42 |
43 | Here is an example `routes.js` file:
44 |
45 | {% highlight javascript %}
46 | var Router = require('bomber/lib/router').Router;
47 | var r = new Router();
48 |
49 | r.add('/:view/:action/:id');
50 | r.add('/:view/:action');
51 |
52 | exports.router = r;
53 | {% endhighlight %}
54 |
55 | Here is an example view file:
56 |
57 | {% highlight javascript %}
58 | exports.index = function(request, response) {
59 | return "index action";
60 | };
61 | exports.show = function(request, response) {
62 | if( request.format == 'json' ) {
63 | return {a: 1, b: 'two', c: { value: 'three'}};
64 | }
65 | else {
66 | return "show action";
67 | }
68 | };
69 | {% endhighlight %}
70 |
71 | Participating
72 | -------------
73 |
74 | We are open to new ideas and feedback, so if you have any thoughts on ways to make Bomber better, or find any bugs, please feel free to [participate in the Google Group](http://groups.google.com/group/bomberjs).
75 |
76 | Relevant Reading
77 | ----------------
78 |
79 | + [Blog post announcing the motivation](http://benjaminthomas.org/2009-11-20/designing-a-web-framework.html)
80 | + [Blog post discussing the design of the routing](http://benjaminthomas.org/2009-11-24/bomber-routing.html)
81 | + [Blog post discussing the design of the actions](http://benjaminthomas.org/2009-11-29/bomber-actions.html)
82 |
83 | [bomber-src]: http://github.com/obt/bomberjs
84 | [Node]: http://nodejs.org/
85 |
--------------------------------------------------------------------------------
/website/docs/action.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: docs
3 | title: Action
4 | ---
5 |
6 | **Note:** I am in the process of discussing some API changes to Actions so check
7 | back here in a week or so to see if things have changed!
8 |
9 | Action objects are instantiated for each request to the server. Their main
10 | goal is making it easy to do asynchronous calls. Really what they boil
11 | down to _at this point_ is a glorified [Deferred](http://api.dojotoolkit.org/jsdoc/1.3.2/dojo.Deferred)
12 | with less functionallity.
13 |
14 | I recommend reading [this blog post](http://benjaminthomas.org/2009-11-29/bomber-actions.html)
15 | for the details of their current design.
16 |
17 | At their simplest, Actions run a series of tasks. A task is just a function. All
18 | tasks receive two parameters, a [request](/docs/request.html) and a
19 | [response](/docs/response.html). Additionally, they receive the return value of
20 | the last task.
21 |
22 | However, if the result of a task is a [Node Promise](http://nodejs.org/api.html#_tt_process_promise_tt),
23 | the action will add the next task as a callback to the promise.
24 |
25 | An `Action` guarantees that all tasks are bound to it. This makes keeping track
26 | of state across tasks easy, just set a variable on the `this` object.
27 |
28 | When all tasks have been run, one of two things happens:
29 |
30 | 1. If there have been no callbacks added to the action, it looks at the return
31 | result of the last task that was ran.
32 |
33 | If it is a `String` the action assumes it is HTML and sends the string as
34 | the response to the client with a content type of 'text/html'.
35 |
36 | If it is any other object, the action converts it to JSON and sends the JSON
37 | string as a response to the client with a content type of 'text/json'. (I
38 | know this is incorrect and I guess I'll change it, but 'text/json' make so
39 | much more sense than 'application/json'!)
40 |
41 | If it is `null` it does nothing. This allows you to manipulate the Node
42 | `http.ServerResponse` object itself for comet style applications or streaming
43 | things to and from the client.
44 |
45 | 2. If a callback has been added to the action, Bomber assumes you want to
46 | actually _do something_ with the result from this action, so it sends nothing
47 | to the client and instead passes the result to the callback.
48 |
49 | Public methods:
50 | ---------------
51 |
52 | `addTask(function)`
53 | : Add a function to the end of the list of tasks to run.
54 |
55 | `insertTask(function)`
56 | : Add a function to the list of tasks to be run _after_ the current task.
57 |
58 | `bindTo(function)`
59 | : Bind a function to this action. Also used internally to make sure
60 | all tasks act on this action.
61 |
62 | `addCallback(function)`
63 | : Add a function that should be run after all the tasks have completed. Is
64 | passed the return value of the last task.
65 |
--------------------------------------------------------------------------------
/lib/request.js:
--------------------------------------------------------------------------------
1 | var querystring = require('querystring');
2 |
3 | /* Request(request, url, route)
4 | *
5 | * A Bomber Request is a wrapper around the node.js `http.ServerRequest` object.
6 | *
7 | * It will basically add some niceties like, waiting for and parsing of POST
8 | * data and easy manipulation of cookies and session variables. But currently
9 | * it does nothing.
10 | *
11 | * Parameters:
12 | *
13 | * + `request`: An `http.ServerRequest` instance.
14 | * + `url`: `Object`: A parsed URL for this request. The server parses the url
15 | * so it can send it to the app to determine the action. We don't want to
16 | * parse it again, so we pass it here.
17 | * + `route`: `Object`. The route object returned from the router. See
18 | * `Router.prototype.findRoute` for details.
19 | */
20 | var Request = exports.Request = function(request, url, route) {
21 | this._request = request;
22 | this._action = route.action;
23 |
24 | // wrap the main ServerRequest properties
25 | this.method = request.method;
26 | this.url = url;
27 | this.headers = request.headers;
28 |
29 | // add our own properties
30 | this.params = process.mixin({}, route.params, this.url.query);
31 | this.data = null;
32 |
33 | //TODO: should we pause the request here and only unpause it in
34 | // this.loadData? Otherwise depending on how people write their actions I
35 | // think we run the risk of losing data.
36 | };
37 |
38 | /* Request.prototype.loadData(parseData)
39 | *
40 | * Returns a promise that is called with the body of this request.
41 | *
42 | * Node doesn't wait for the entire body of a request to be received before
43 | * starting the callback for a given request. Instead we have to listen for
44 | * 'body' and 'complete' events on the request to receive and know when the
45 | * entire request has been loaded. That's what this function does.
46 | *
47 | * Additionally `Request.prototype.loadData` will parse the body of the
48 | * request using `querystring.parse()`. This can be turned off with `parseData`
49 | * parameter.
50 | *
51 | * Parameters:
52 | *
53 | * + `parseData`: `Boolean`. Whether or not to parse the loaded data with
54 | * `querystring.parse()`. Default is true.
55 | *
56 | * Returns:
57 | *
58 | * A Promise that will be fullfilled with the loaded (and maybe parsed data)
59 | * for this request.
60 | */
61 | Request.prototype.loadData = function(parseData) {
62 | if( typeof parseData == 'undefined' ) {
63 | parseData = true;
64 | }
65 |
66 | var p = new process.Promise();
67 | var self = this;
68 |
69 | if( this.data === null ) {
70 | var data = '';
71 |
72 | this._request.addListener('body', function(chunk) {
73 | data += chunk;
74 | });
75 |
76 | this._request.addListener('complete', function() {
77 | if( parseData ) {
78 | self.data = querystring.parse(data);
79 | }
80 | else {
81 | self.data = data;
82 | }
83 | p.emitSuccess(self.data);
84 | });
85 | }
86 | else {
87 | process.nextTick(function() {
88 | p.emitSuccess(self.data);
89 | });
90 | }
91 |
92 | return p;
93 | };
94 |
--------------------------------------------------------------------------------
/dependencies/node-httpclient/README:
--------------------------------------------------------------------------------
1 | A simple to use (hopefully) http client for the node.js platform.
2 | It adds easy control of HTTPS, gzip compression and cookie handling
3 | to the basic http functionality provided by node.js http library.
4 |
5 | Dependencies:
6 | - A patched version of the http.js node module is included with the
7 | source. The diff for this patch against node release 0.1.26 can
8 | be found here:
9 | http://gist.github.com/293534
10 |
11 | The patch returns the response headers as an array instead of as
12 | a comma separated string
13 |
14 | - if you want to use gzip you will need the node-compress module
15 | built and available in the lib directory (or the same directory
16 | as httpclient.js). node-compress is here:
17 |
18 | http://github.com/waveto/node-compress
19 |
20 | Todo:
21 | - better Error handling
22 | - full set of http client tests to compare with other http clients
23 | (e.g. libcurl, libwww, winhttp)
24 | - handle all cookies correctly according to RFC
25 | - allow saving and restoring of cookies
26 | - handle gzip encoding from client to server (should be simple)
27 | - allow specification of timeouts for connection and response
28 | - allow explicit closing of connection
29 | - allow option to automatically follow redirects (should be a simple change)
30 | - property content handling (binary, text encodings etc) based on http headers
31 | - handle http expiration headers and cacheing policies
32 | - handle head, put and delete requests
33 | etc, etc, etc...
34 |
35 | Note:
36 | - currently, the httpclient object will only allow one request at a time
37 | for a given protocol and host name (e.g. http://www.google.com). this
38 | behaviour is handled in http.js where requests get queued up and executed
39 | in sequence. if you want to send parallel requests to the same site, then
40 | create a new httpclient object for each of the parallel requests. Am not
41 | sure whether this is the correct behaviour but it seems to work well for
42 | what i need right now.
43 |
44 | Usage:
45 |
46 | (see example.js)
47 |
48 | var sys = require("sys");
49 | var httpcli = require("./httpclient");
50 |
51 | var url = "http://www.betfair.com";
52 | var surl = "https://www.betfair.com";
53 |
54 | function verifyTLS(request) {
55 | sys.puts(sys.inspect(request));
56 | return true;
57 | }
58 |
59 | var client = new httpcli.httpclient();
60 |
61 | // a simple http request with default options (gzip off, keepalive off, https off)
62 | client.perform(url, "GET", function(result) {
63 | sys.puts(sys.inspect(result));
64 | }, null);
65 |
66 | var client2 = new httpcli.httpclient();
67 |
68 | // nested calls with gzip compression and keep-alive
69 | client2.perform(url, "GET", function(result) {
70 | sys.puts(sys.inspect(result));
71 | client2.perform(url, "GET", function(result) {
72 | sys.puts(sys.inspect(result));
73 | // https request with callback handling of certificate validation
74 | client2.perform(surl, "GET", function(result) {
75 | sys.puts(sys.inspect(result));
76 | }, null, {"Accept-Encoding" : "none,gzip", "Connection" : "close"}, verifyTLS);
77 | }, null, {"Accept-Encoding" : "none,gzip", "Connection" : "Keep-Alive"});
78 | }, null, {"Accept-Encoding" : "none,gzip", "Connection" : "Keep-Alive"});
79 |
80 |
81 |
--------------------------------------------------------------------------------
/exampleProject/views/simple.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var posix = require('posix');
3 | var path = require('path');
4 |
5 | var HTTP301MovedPermanently = require('bomberjs/lib/http_responses').HTTP301MovedPermanently;
6 |
7 | function htmlHead () {
8 | return 'Bomber.js example app';
9 | }
10 | function htmlFoot () {
11 | return '';
12 | }
13 |
14 | // can be accessed at '/'
15 | exports.index = function(request, response) {
16 | response.cookies.set('hello','world');
17 |
18 | var views = response.session.get('index_views') + 1;
19 | response.session.set('index_views', views);
20 |
21 | var html = htmlHead();
22 | html += "index action
See routes.js for more.
";
23 | html += "Other actions include:
";
24 | html += "";
30 |
31 | html += "You have viewed this page " + views + (views == 1 ? " time" : " times");
32 |
33 | html += htmlFoot();
34 | return html;
35 | };
36 |
37 | exports.env = function(request, response) {
38 | var views = response.session.get('env_views') + 1;
39 | response.session.set('env_views', views);
40 |
41 | var html = htmlHead();
42 |
43 | html += "
Env Action
";
44 |
45 | html += "Currently set cookies
";
46 | html += "";
47 | request.cookies.keys().forEach(function(key) {
48 | html += "- "+key+"
"+response.cookies.get(key)+"";
49 | });
50 | html += "
";
51 |
52 | html += "Currently set session vars
";
53 | html += "";
54 | request.session.keys().forEach(function(key) {
55 | html += "- "+key+"
"+response.session.get(key)+"";
56 | });
57 | html += "
";
58 |
59 | html += "You have viewed this page " + views + (views == 1 ? " time" : " times");
60 |
61 | html += htmlFoot();
62 |
63 | return html;
64 | }
65 |
66 | // can be accessed at '/section/'
67 | exports.show = function(request, response) {
68 | var views = response.session.get('show_views') + 1;
69 | response.session.set('show_views', views);
70 |
71 | if( request.params.id == 2 ) {
72 | return new HTTP301MovedPermanently('http://google.com');
73 | }
74 | else {
75 | var html = htmlHead();
76 | html += "Show Action
";
77 | html += "You have viewed this page " + views + (views == 1 ? " time" : " times");
78 | html += htmlFoot();
79 | return html;
80 | }
81 | };
82 |
83 | // can be accessed at /simple/lorem
84 | exports.lorem = function(request, response) {
85 | // this example shows some asynchronous stuff at work!
86 | response.session.set('lorem_views', response.session.get('lorem_views') + 1);
87 |
88 | response.mimeType = 'text/plain';
89 |
90 | var filename = path.join(path.dirname(__filename),'../resources/text.txt');
91 |
92 | // posix.cat returns a Node promise. Which at this time isn't chainable
93 | // so this example is pretty simple. But once we can chain, I'll show
94 | // how to manipulate the result as you move through the chain.
95 | return posix.cat(filename);
96 | };
97 |
--------------------------------------------------------------------------------
/lib/http_responses.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils');
2 |
3 | exports.HTTPResponse = function(body) {
4 | this.body = body;
5 | this.status = null;
6 | this.mimeType = 'text/html';
7 | };
8 | exports.HTTPResponse.prototype.respond = function(response) {
9 | if( this.status === null ) {
10 | this.status = this.name.substr(4,3);
11 | }
12 | response.status = this.status;
13 | response.mimeType = this.mimeType;
14 | response.send(this.body || this.name || '');
15 | if( !response.finishOnSend ) {
16 | response.finish();
17 | }
18 | };
19 |
20 | var http_responses = {};
21 |
22 | http_responses['200OK'] = null;
23 | http_responses['201Created'] = null;
24 | http_responses['202Accepted'] = null;
25 | http_responses['203NonAuthoritativeInformation'] = null;
26 | http_responses['204NoContent'] = null;
27 | http_responses['205ResetContent'] = null;
28 | http_responses['206PartialContent'] = null;
29 |
30 | http_responses['300MultipleChoices'] = null;
31 | http_responses['304NotModified'] = null;
32 |
33 | http_responses['__redirect'] = function(url, status) {
34 | this.url = url;
35 | if( typeof status == 'undefined' ) {
36 | this.status = 301;
37 | }
38 | else {
39 | this.status = status;
40 | }
41 | };
42 | http_responses['__redirect'].prototype.respond = function(response) {
43 | response.status = this.status;
44 | response.headers['Location'] = this.url;
45 | response.finish();
46 | };
47 | ['301MovedPermanently', '302Found', '303SeeOther', '307TemporaryRedirect'].forEach(function(name) {
48 | http_responses[name] = function(url) {
49 | this.url = url;
50 | this.status = name.substr(0,3);
51 | };
52 | http_responses[name].prototype.respond = http_responses.__redirect.prototype.respond;
53 | });
54 |
55 | http_responses['400BadRequest'] = null;
56 | http_responses['401Unauthorized'] = null;
57 | http_responses['402PaymentRequired'] = null;
58 | http_responses['403Forbidden'] = null;
59 | http_responses['404NotFound'] = null;
60 | http_responses['405MethodNotAllowed'] = null;
61 | http_responses['406NotAcceptable'] = null;
62 | http_responses['407ProxyAuthenticationRequired'] = null;
63 | http_responses['408RequestTimeout'] = null;
64 | http_responses['409Conflict'] = null;
65 | http_responses['410Gone'] = null;
66 | http_responses['411LengthRequired'] = null;
67 | http_responses['412PreconditionFailed'] = null;
68 | http_responses['413RequestEntityTooLarge'] = null;
69 | http_responses['414RequestURITooLong'] = null;
70 | http_responses['415UnsupportedMediaType'] = null;
71 | http_responses['416RequestedRangeNotSatisfiable'] = null;
72 | http_responses['417ExpectationFailed'] = null;
73 | http_responses['418ImATeapot'] = null;
74 |
75 | http_responses['500InternalServerError'] = null;
76 | http_responses['501NotImplemented'] = null;
77 | http_responses['502BadGateway'] = null;
78 | http_responses['503ServiceUnavailable'] = null;
79 | http_responses['504GatewayTimeout'] = null;
80 | http_responses['509BandwidthLimitExceeded'] = null;
81 |
82 | for( var err_name in http_responses ) {
83 | if( http_responses[err_name] ) {
84 | var func = http_responses[err_name];
85 | }
86 | else {
87 | var func = function() {
88 | exports.HTTPResponse.apply(this, arguments);
89 | };
90 | }
91 | func.prototype.__proto__ = exports.HTTPResponse.prototype;
92 | func.prototype.name = 'HTTP'+err_name;
93 | exports['HTTP'+err_name] = func;
94 | };
95 |
96 | exports.forbidden = exports.HTTP403Forbidden;
97 | exports.notFound = exports.HTTP404NotFound;
98 |
99 | // we declared this above because we wanted it to properly
100 | // "extend" HTTPResponse. But the above adds the 'HTTP' prefix
101 | // so, get rid of it.
102 | exports.redirect = exports.HTTP__redirect;
103 | delete exports.HTTP__redirect;
104 |
--------------------------------------------------------------------------------
/bomber.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | /* bomber.js
4 | *
5 | * bomber.js is a shell script that is used to manage your Bomber projects.
6 | *
7 | * Syntax:
8 | * -------
9 | *
10 | * bomber.js
11 | *
12 | * Flags:
13 | *
14 | * + `--app `, `-a `: Optional. the name or the path of a
15 | * Bomber app. This argument is used by the Node require command, so read up
16 | * on how that works to make sure Bomber will be able to find your app. If the
17 | * argument isn’t supplied it uses the current directory.
18 | *
19 | * Tasks:
20 | *
21 | * It loads its commands in from `./lib/tasks`.
22 | *
23 | * + `start-server`: Start a bomber server
24 | * + `run-tests`: Run all javascript files in the `test` folder that start with
25 | * `test-`
26 | *
27 | * Examples:
28 | * ---------
29 | *
30 | * ./bomber.js start-server
31 | *
32 | * ./bomber.js --app ./exampleProject start-server
33 | */
34 |
35 | var sys = require('sys');
36 | var path = require('path');
37 | var posix = require('posix');
38 |
39 | // We depend on bomberjs being on the path, so wrap this require
40 | // for a module we need in a try catch, and so if bomberjs isn't in
41 | // the path we can add it.
42 | try {
43 | var App = require('bomberjs/lib/app').App;
44 | }
45 | catch(err) {
46 | // if that isn't on the path then assume the script being run is
47 | // in the source, so add the directory of the script to the path.
48 | if( err.message == "Cannot find module 'bomberjs/lib/app'" ) {
49 | require.paths.push(path.dirname(__filename)+'/..');
50 | var App = require('bomberjs/lib/app').App;
51 | }
52 | }
53 |
54 | // load in all the files from bomberjs/lib/tasks. These are our
55 | // base tasks that every app has. like start_server (and
56 | // run_tests eventually). Down the line I'd like apps to be able
57 | // to write their own tasks
58 | var bomberjs_location = path.normalize(path.join(require('bomberjs/lib/utils').require_resolve('bomberjs/lib/app'),'../../'));
59 | var dir = posix.readdir(path.join(bomberjs_location,'lib/tasks')).wait();
60 | var tasks = {};
61 | dir.forEach(function(file) {
62 | if( !file.match(/\.js$/) ) {
63 | return;
64 | }
65 | var p = 'bomberjs/lib/tasks/'+file.substr(0,file.length-3);
66 | tasks[path.basename(p)] = p;
67 | });
68 |
69 |
70 | // ARGV[0] always equals node
71 | // ARGV[1] always equals bomber.js
72 | // so we ignore those
73 | var argv = process.ARGV.slice(2);
74 |
75 | // parse arguments
76 | var opts = {};
77 | while( argv.length > 0 ) {
78 | var stop = false;
79 | switch(argv[0]) {
80 | case "--tasks":
81 | case "-t":
82 | opts['tasks'] = true;
83 | break;
84 | case "--app":
85 | case "-a":
86 | opts['app'] = argv[1];
87 | argv.splice(0,1);
88 | break;
89 | default:
90 | stop = true;
91 | }
92 | if( stop ) {
93 | break;
94 | }
95 | argv.splice(0,1);
96 | }
97 |
98 | // the global scope that all apps (and objects, I guess) will have access too.
99 | var project = {config: {}};
100 |
101 | // create the base app for the project
102 | project.base_app = new App(opts.app, project);
103 |
104 | //TODO: allow apps to be able to supply their own tasks and load them
105 | // here
106 |
107 | if( opts.tasks ) {
108 | sys.puts('Available tasks:');
109 | Object.keys(tasks).forEach(function(task) {
110 | sys.print(" ");
111 | sys.puts(task);
112 | });
113 | }
114 | else if( argv.length == 0) {
115 | sys.puts("Script for managing Bomber apps.\nhttp://bomber.obtdev.com/");
116 | }
117 | else {
118 | if( !(argv[0] in tasks) ) {
119 | sys.puts("Unknown task: "+argv[0]);
120 | }
121 | else {
122 | var task = argv.shift();
123 | require(tasks[task]).task(project, argv);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/exampleProject/resources/text.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum ठः अ ठी ३ dolor üit amet, conséctetur adipiscing elit. I
2 | teger non tempus eros. Phasellus et lacus ligula, in placerat massa. Quisque
3 | sit amet odio a purus semper congue id at ligula. In hac habitasse platea
4 | dictumst. Mauris sit amet turpis eros, at scelerisque sem. Vestibulum ante ipsum
5 | primis in faucibus orci luctus et ultrices posuere cubilia Curae; Cras sed odio
6 | orci. Phasellus at dolor a tortor imperdiet feugiat. Quisque blandit, quam sed
7 | cursus cursus, ipsum odio placerat massa, id scelerisque lacus nisi rhoncus
8 | velit. Vivamus vitae enim enim, a laoreet nunc. Phasellus eleifend erat non
9 | lorem facilisis in varius purus euismod. Aenean sit amet risus at lectus rhoncus
10 | pulvinar vitae porttitor orci. Nulla adipiscing dui eu elit vestibulum at dictum
11 | lectus condimentum. Mauris eu volutpat mi. Proin quis est eget lacus
12 | ullamcorper aliquet eget et lorem. Vestibulum nec urna vel odio semper aliquet.
13 |
14 | Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
15 | himenaeos. Curabitur et elit lobortis sem aliquam bibendum. Fusce vel dui eu
16 | lorem consectetur venenatis. Nunc felis nunc, convallis sit amet dignissim eget,
17 | dictum et metus. Aenean scelerisque blandit tempor. In rhoncus, risus at mattis
18 | facilisis, est nunc tempus turpis, iaculis lobortis turpis sapien vitae dolor.
19 | Nam at ligula ac mauris ultricies rhoncus et sollicitudin est. Morbi et mi eget
20 | est mattis dictum. Phasellus purus magna, mattis et consequat auctor, placerat
21 | ut erat. Pellentesque habitant morbi tristique senectus et netus et malesuada
22 | fames ac turpis egestas. Vivamus venenatis vulputate molestie. Duis nisl nisi,
23 | elementum ut iaculis quis, dignissim lacinia nunc. Donec posuere elit eu metus
24 | ullamcorper ac porttitor urna dapibus. Nulla vel felis orci. Etiam
25 | consequat risus nibh.
26 |
27 | Morbi consequat est vel quam luctus ultrices. Cras consequat mattis dapibus. In
28 | hac habitasse platea dictumst. Fusce tempor blandit massa, vitae sollicitudin
29 | ipsum feugiat imperdiet. In convallis arcu a nisi ultricies pellentesque. Proin
30 | in nibh lorem. Phasellus accumsan molestie rhoncus. In hac habitasse platea
31 | dictumst. Vestibulum eu semper diam. Morbi a est eget orci egestas rutrum sit
32 | amet quis nunc. Curabitur vel nulla eget nisl pharetra facilisis et fermentum
33 | ligula. Donec orci lorem, egestas quis auctor sit amet, accumsan vitae diam.
34 | Praesent lorem erat, gravida eu rutrum ut, tincidunt sagittis justo. Nunc
35 | molestie pharetra massa quis lobortis. Nunc quis sapien in mauris aliquam
36 | hendrerit vitae in erat.
37 |
38 | Praesent justo ligula, tempor blandit aliquet tincidunt, malesuada in urna.
39 | Aliquam et odio mauris, et dictum tortor. Vestibulum eu orci vel ante pharetra
40 | imperdiet accumsan a neque. Donec sit amet massa consequat est dapibus pharetra
41 | eget nec leo. Quisque ultricies viverra ipsum ut euismod. Praesent commodo ante
42 | convallis quam elementum sit amet venenatis lorem hendrerit. Fusce ut erat nibh.
43 | Morbi sit amet velit eu enim vehicula ullamcorper a quis urna. Nullam sed odio
44 | nulla, sed lobortis nisl. Nulla facilisi.
45 |
46 | Vivamus sed dapibus velit. Mauris tristique pellentesque tortor vel
47 | pellentesque. Curabitur bibendum mauris vel mauris tempor vulputate. Integer
48 | porta, massa quis scelerisque dictum, lorem libero placerat leo, vitae
49 | condimentum mauris nisi id arcu. Donec non dui varius quam bibendum fermentum in
50 | dictum arcu. Etiam et orci neque. In quis nisi ipsum, hendrerit commodo lacus.
51 | In vitae turpis ligula, quis placerat nisl. Maecenas tincidunt orci vitae metus
52 | imperdiet porta. Nullam eu dolor nisl, auctor ullamcorper urna. Etiam sit amet
53 | fringilla orci. Mauris eget ipsum ipsum, quis ornare tellus. Suspendisse
54 | ullamcorper metus felis. Nulla non enim nisl, a dapibus odio. Nulla facilisi. In
55 | tellus est, luctus et egestas non, aliquet ac turpis.
56 |
--------------------------------------------------------------------------------
/lib/action.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 |
3 | var Promise = require('./promise').Promise;
4 |
5 | var HTTPResponse = require('./http_responses').HTTPResponse,
6 | utils = require('./utils');
7 |
8 | /* processAction(request, response, action)
9 | *
10 | * This handles actually running an action. If the response from the action is a
11 | * Promise, `complete` or `err` are added as callbacks. Otherwise they are called
12 | * directly with the response.
13 | *
14 | * Parameters:
15 | *
16 | * + `request`: a Bomberjs Request object
17 | * + `response`: a Bomberjs Response object
18 | * + `action`: a function that is to be run
19 | */
20 | exports.processAction = function(request, response, action) {
21 | var action_details = {
22 | request: request,
23 | response: response
24 | };
25 |
26 | var complete_handler = utils.bind(complete, action_details);
27 | var err_handler = utils.bind(err, action_details);
28 |
29 | //TODO before filters?
30 |
31 | try {
32 | var action_response = action(request, response);
33 | }
34 | catch(err) {
35 | err_handler(err);
36 | }
37 |
38 | if( action_response instanceof process.Promise
39 | || action_response instanceof Promise ) {
40 | action_response.addCallback(complete_handler);
41 | action_response.addErrback(err_handler);
42 | }
43 | else {
44 | complete_handler(action_response);
45 | }
46 | };
47 |
48 | /* err(err)
49 | *
50 | * At some point in the course of the action an object was thrown. If it is
51 | * an HTTPResponse, then call `respond()` on it, otherwise this must be an
52 | * error, return a status 500 response
53 | *
54 | * Parameters:
55 | *
56 | * + `err`: The object that was thrown (presumably an error)
57 | */
58 | function err(err) {
59 | if( err instanceof HTTPResponse ) {
60 | err.respond(this.response);
61 | }
62 | else {
63 | if( !this.response._finished ) {
64 | if( !this.response._sentHeaders ) {
65 | this.response.status = 500;
66 | this.mimeType = 'text/plain';
67 | }
68 |
69 | this.response.send('500 error!\n\n' + (err.stack || err));
70 |
71 | if( !this.response.finishOnSend ) {
72 | this.response.finish();
73 | }
74 | }
75 | }
76 |
77 | return null;
78 | }
79 |
80 | /* complete(action_response)
81 | *
82 | * Is called from `process_action` with the response from an action
83 | *
84 | * Can only take one argument, because functions (actions) can only return
85 | * one thing.
86 | *
87 | * The role of this function is to decide if the action returned something that
88 | * needs to be sent to the client. There are three cases:
89 | *
90 | * 1. The action didn't return anything. In this case we assume the action
91 | * took care of sending its response itself.
92 | * 2. The action returned an object that is an instance of HTTPResponse, aka it
93 | * returned a prebuilt response with a `respond()` function. so just call
94 | * `respond()` on that object.
95 | * 3. The action returned something else. In this case we want to send this to
96 | * the client. If it is a string, send it as it is, and if it is anything
97 | * else convert it to a JSON string and then send it.
98 | *
99 | * Parameters:
100 | *
101 | * + `action_response`: the response from the action.
102 | */
103 | function complete(action_response) {
104 | if( typeof action_response == "undefined" ) {
105 | // Do nothing. The action must have taken care of sending a response.
106 | }
107 | else if( action_response instanceof HTTPResponse ) {
108 | action_response.respond(this.response);
109 | }
110 | else {
111 | //otherwise send a response to the client
112 | if(action_response.constructor == String) { // return text/html response
113 | this.response.send(action_response);
114 | }
115 | else { // return a json representation of the object
116 | this.response.mimeType = 'application/json';
117 | this.response.send(sys.inspect(action_response));
118 | }
119 | }
120 | };
121 |
--------------------------------------------------------------------------------
/lib/server.js:
--------------------------------------------------------------------------------
1 | // node modules
2 | var sys = require('sys'),
3 | http = require('http'),
4 | url = require('url');
5 |
6 | // dependency modules
7 | var sha1 = require('../dependencies/sha1');
8 |
9 | // bomber modules
10 | var Response = require('./response').Response,
11 | Request = require('./request').Request,
12 | processAction = require('./action').processAction,
13 | Cookies = require('./cookies').Cookies,
14 | SessionManager = require('./session').SessionManager;
15 |
16 | /* Server(project)
17 | *
18 | * A Server uses Nodes `http` module to listen for HTTP requests on a given
19 | * port, and then parses those requests and uses the passed in app to find an
20 | * action for the request, and then processes that aciton.
21 | *
22 | * Parameters:
23 | *
24 | * + `app`: Bomberjs `App` object. The app to hand the incoming HTTP
25 | * requests to.
26 | */
27 | var Server = exports.Server = function(project, config) {
28 | this.project = project;
29 | project.server = this;
30 |
31 | // grab the server settings from the project
32 | this.project.config.server = process.mixin(Server.__defaultOptions, this.project.config.server, config);
33 |
34 | // Warn if no signing secret is set -- and create a temporary one
35 | if (!this.project.config.security || !this.project.config.security.signing_secret) {
36 | sys.error( "\nWARNING: No signing secret is set in your configuration file.\n" +
37 | " The secret is used to sign secure cookies, validate session and encrypt user\n"+
38 | " passwords, so it's extremely important for you to set a unique and secure secret.\n" +
39 | " A temporary secret will be used for now, but all cookies, session and user\n" +
40 | " accounts will be invalidated when the server is restarted!\n" );
41 |
42 | // set the random signing secret
43 | this.project.security = this.project.security || {};
44 | this.project.security.signing_secret = sha1.hex_hmac_sha1(Math.random(), Math.random());
45 | }
46 |
47 | this.session_manager = new SessionManager(this.project.config.server.sessions);
48 | };
49 |
50 | /* Server.__defaultOoptions
51 | *
52 | * The default options for the server.
53 | *
54 | * This gets merged with the options specified for the server in the project config
55 | */
56 | Server.__defaultOptions = {
57 | port: 8400,
58 | sessions: {
59 | storage_method: 'disk',
60 | disk_storage_location: '/tmp/',
61 | expire_minutes: 600,
62 | renew_minutes: 10,
63 | cookie: {
64 | name: 'session_key',
65 | domain: '',
66 | path: '/',
67 | secure: false
68 | }
69 | }
70 | };
71 |
72 | /* Server.prototype.stop()
73 | *
74 | * Stop listening.
75 | */
76 | Server.prototype.stop = function() {
77 | this.httpServer.close();
78 | };
79 |
80 | /* Server.prototype.start()
81 | *
82 | * Start listening.
83 | */
84 | Server.prototype.start = function() {
85 | var server = this;
86 | var base_app = this.project.base_app;
87 |
88 | this.httpServer = http.createServer(function (req, res) {
89 | try {
90 | sys.puts("\nReceived " + req.method + " request for " + req.url);
91 |
92 | // Routers only get a path, we parse the url now so it only happens once
93 | var parsedURL = url.parse(req.url, true);
94 |
95 | var route = base_app.getRoute(req.method, parsedURL.pathname);
96 | if(!route) { return server.send404(res, parsedURL.pathname); }
97 |
98 | // wrap the request and the response
99 | var request = new Request(req, parsedURL, route);
100 | var response = new Response(res);
101 | // Append cookies and sessions objects to request and response
102 | request.cookies = response.cookies = new Cookies(request, response, server.project);
103 | request.session = response.session = server.session_manager.getSession(request);
104 |
105 | // get the action designated by the route, and run it
106 | var action = base_app.getAction(route.action);
107 | processAction(request, response, action);
108 |
109 | // Finish the session (this is unlikely to be the perfect place for it, at last if we're running stuff async)
110 | // There's probably a need for a way to specify tasks/callbacks after the action has run anyway.
111 | request.session.finish();
112 | }
113 | catch(err) {
114 | res.sendHeader(500, {'Content-Type': 'text/plain'});
115 | res.sendBody('500 error!\n\n' + (err.stack || err));
116 | res.finish();
117 | }
118 | });
119 | this.httpServer.listen(this.project.config.server.port);
120 |
121 | sys.puts('Bomber Server running at http://localhost:'+this.project.config.server.port);
122 | };
123 |
124 | /* Server.prototype.send404()
125 | *
126 | * In the event that a route can't be found for a request, this function is
127 | * called to send the HTTP 404 response.
128 | *
129 | * Parameters:
130 | *
131 | * `res`: A Node `http.serverResponse` object.
132 | * `path`: The path for which a route couldn't be found.
133 | */
134 | Server.prototype.send404 = function(res, path) {
135 | res.sendHeader(404, {'Content-Type': 'text/plain'});
136 | var body = 'Not found: ' + path;
137 | if( this.project.base_app.router ) {
138 | body += '\n\nRoutes tried:';
139 | this.project.base_app.router._routes.forEach(function(route) {
140 | body += '\n ' + route.regex;
141 | });
142 | }
143 | res.sendBody(body);
144 | res.finish();
145 | }
146 |
147 |
--------------------------------------------------------------------------------
/dependencies/node-async-testing/README:
--------------------------------------------------------------------------------
1 | A simple test runner with testing asynchronous code in mind.
2 |
3 | Some goals of the project:
4 |
5 | + I want it to be simple. You create a test and then run the code you want to
6 | test and make assertions as you go along. Tests should be functions.
7 | + I want to use the assertion module that comes with Node. So, if you are
8 | familiar with those you won't have any problems. You shouldn't have to learn
9 | new assertion functions.
10 | + I want test files to be executable by themselves. So, if your test file is
11 | called "my_test_file.js" then "node my_test_file.js" should run the tests.
12 | + Address the issue of testing asynchronouse code. Node is asynchronous, so
13 | testing should be too.
14 | + I don't want another behavior driven development testing framework. I don't
15 | like specifications and what not. They only add verbosity.
16 |
17 | test('X does Y',function() {
18 | //test goes here
19 | });
20 |
21 | is good enough for me.
22 | + I'd like it to be as general as possible so you can test anything.
23 |
24 | Feedback/suggestions encouraged!
25 |
26 | ------------------------------------------------------------
27 |
28 | The hard part of writing a test suite for asynchronous code is that when a test
29 | fails, you don't know which test it was that failed.
30 |
31 | This module aims to address that issue by giving each test its own unique assert
32 | object. That way you know which assertions correspond to which tests.
33 |
34 | test('my test name', function(test) {
35 | test.assert.ok(true);
36 | });
37 |
38 | Because you don't know how long the asynchronous code is going to take, no
39 | results are printed about the tests until the process exits and we know all the
40 | tests are finished. It would be confusing to be printing the results of tests
41 | willy nilly. This way, you get the results in the order that the tests are
42 | written in the file.
43 |
44 | The output looks something like this:
45 |
46 | Starting test "this does something" ...
47 | Starting test "this doesn't fail" ...
48 | Starting test "this does something else" ...
49 | Starting test "this fails" ...
50 | Starting test "throws" ...
51 |
52 | Results:
53 | ...F.
54 |
55 | test "this fails" failed: AssertionError: true == false
56 | at [object Object].ok (/path/to/node-asyncTesting/asyncTesting.js:21:29)
57 | at Timer. (/path/to/node-simpletests/testsExample.js:25:25)
58 | at node.js:988:1
59 | at node.js:992:1
60 |
61 | There is also a TestSuite object:
62 |
63 | var ts = new TestSuite('Name');
64 | ts.setup = function() {
65 | this.foo = 'bar';
66 | }
67 | ts.runTests({
68 | "foo equals bar": function(test) {
69 | test.assert.equal('bar', test.foo);
70 | }
71 | });
72 |
73 | The setup function is ran once for each test. You can also add a
74 | teardown function:
75 |
76 | var ts = new TestSuite('Name');
77 | ts.teardown = function() {
78 | this.foo = null;
79 | }
80 |
81 | Tests suites output a little more information:
82 |
83 | > Starting tests for "Name"
84 | > Starting test "foo equals bar" ...
85 | >
86 | > Ran suite "Name"
87 | > .
88 | > 1 test; 0 failures; 1 assertion
89 |
90 | There is a convenience method so if you do know when the test is finished you
91 | can call `test.finish()`. Then if all the tests in the suite are done it will
92 | immediately output as opposed to waiting for the process to exit. Currently,
93 | only test suites are able to take advantage of this because they know exactly
94 | how many tests exist.
95 |
96 | (new TestSuite()).runTests({
97 | "foo equals bar": function(test) {
98 | test.assert.equal('bar', test.foo);
99 | test.finish();
100 | }
101 | });
102 |
103 | Additionally, if the order of the tests does matter, you can tell the TestSuite
104 | to wait for each test to finish before starting the next one. This requires you
105 | to use the aforementioned function to explicitly indicate when the test
106 | is finished.
107 |
108 | var count = 0;
109 | var ts = new TestSuite('Wait');
110 | ts.wait = true;
111 | ts.runTests({
112 | "count equal 0": function(test) {
113 | test.assert.equal(0, count);
114 | setTimeout(function() {
115 | count++;
116 | test.finish();
117 | }, 50);
118 | },
119 | "count equal 1": function(test) {
120 | test.assert.equal(1, count);
121 | test.finish();
122 | }
123 | });
124 |
125 | Finally, if you want to be explicit about the number of assertions run in a
126 | given test, you can set `numAssertionsExpected` on the test. This can be helpful
127 | in asynchronous tests where you want to be sure all the assertions are ran.
128 |
129 | test('my test name', function(test) {
130 | test.numAssertionsExpected = 3;
131 | test.assert.ok(true);
132 | // this test will fail
133 | });
134 |
135 | Currently there is no way to know when an error is thrown which test it came
136 | from (note, I am not referring to failures here but unexpected errors). This
137 | could be addressed if you require all async code to be in Promises
138 | or smething similar (like Spectacular http://github.com/jcrosby/spectacular
139 | does), but I am not ready to make that requirement right now. If I say
140 | setTimeout(func, 500); and that function throws an error, there is no way for me
141 | to know which test it corresponds to. So, currently, if an error is thrown, the
142 | TestSuite or file exits there.
143 |
--------------------------------------------------------------------------------
/dependencies/node-httpclient/lib/httpclient.js:
--------------------------------------------------------------------------------
1 | var http = require("./http");
2 | var url = require("url");
3 | var sys = require("sys");
4 | var events = require("events");
5 |
6 | try {
7 | var compress = require("./compress");
8 | }
9 | catch(err) {
10 | if( err.message.indexOf("Cannot find module") >= 0 ) {
11 | var compress = null;
12 | }
13 | else {
14 | throw err;
15 | }
16 | }
17 |
18 | function httpclient() {
19 | var cookies = [];
20 |
21 | if( compress !== null ) {
22 | var gunzip = new compress.Gunzip;
23 | }
24 |
25 | var clients = {
26 | };
27 |
28 | this.clients = clients;
29 |
30 | this.perform = function(rurl, method, cb, data, exheaders, tlscb) {
31 | this.clientheaders = exheaders;
32 | var curl = url.parse(rurl);
33 | var key = curl.protocol + "//" + curl.hostname;
34 |
35 | if(!clients[key]) {
36 | var client = null;
37 | if(!curl.port) {
38 | switch(curl.protocol) {
39 | case "https:":
40 | client = http.createClient(443, curl.hostname);
41 | client.setSecure("x509_PEM");
42 | break;
43 | default:
44 | client = http.createClient(80, curl.hostname);
45 | break;
46 | }
47 | }
48 | else {
49 | client = http.createClient(parseInt(curl.port), curl.hostname);
50 | }
51 | clients[key] = {
52 | "http": client,
53 | "headers": {}
54 | };
55 | }
56 |
57 | clients[key].headers = {
58 | "User-Agent": "Node-Http",
59 | "Accept" : "*/*",
60 | "Connection" : "close",
61 | "Host" : curl.hostname
62 | };
63 | if(method == "POST") {
64 | clients[key].headers["Content-Length"] = data.length;
65 | clients[key].headers["Content-Type"] = "application/x-www-form-urlencoded";
66 | }
67 | for (attr in exheaders) { clients[key].headers[attr] = exheaders[attr]; }
68 |
69 | var mycookies = [];
70 |
71 | cookies.filter(function(value, index, arr) {
72 | if(curl.pathname) {
73 | return(curl.hostname.substring(curl.hostname.length - value.domain.length) == value.domain && curl.pathname.indexOf(value.path) >= 0);
74 | }
75 | else {
76 | return(curl.hostname.substring(curl.hostname.length - value.domain.length) == value.domain);
77 | }
78 | }).forEach( function(cookie) {
79 | mycookies.push(cookie.value);
80 | });
81 | if( mycookies.length > 0 ) {
82 | clients[key].headers["Cookie"] = mycookies.join(";");
83 | }
84 |
85 | var target = "";
86 | if(curl.pathname) target += curl.pathname;
87 | if(curl.search) target += curl.search;
88 | if(curl.hash) target += curl.hash;
89 | if(target=="") target = "/";
90 |
91 | var req = clients[key].http.request(method, target, clients[key].headers);
92 |
93 | if(method == "POST") {
94 | req.sendBody(data);
95 | }
96 |
97 | req.finish(function(res) {
98 | var mybody = [];
99 | if(tlscb) {
100 | if(!tlscb({
101 | "status" : res.connection.verifyPeer(),
102 | "certificate" : res.connection.getPeerCertificate("DNstring")
103 | }
104 | )) {
105 | cb(-2, null, null);
106 | return;
107 | }
108 | }
109 | res.setBodyEncoding("utf8");
110 | if(res.headers["content-encoding"] == "gzip") {
111 | res.setBodyEncoding("binary");
112 | }
113 | res.addListener("body", function(chunk) {
114 | mybody.push(chunk);
115 | });
116 | res.addListener("complete", function() {
117 | var body = mybody.join("");
118 | if( compress !== null && res.headers["content-encoding"] == "gzip") {
119 | gunzip.init();
120 | body = gunzip.inflate(body, "binary");
121 | gunzip.end();
122 | }
123 | var resp = {
124 | "response": {
125 | "status" : res.statusCode,
126 | "headers" : res.headers,
127 | "body-length" : body.length,
128 | "body" : body
129 | },
130 | "request": {
131 | "url" : rurl,
132 | "headers" : clients[key].headers
133 | }
134 | }
135 | cb(resp);
136 | });
137 | res.addListener("error", function() {
138 | cb(-1, res.headers, mybody.join(""));
139 | });
140 | if(res.headers["set-cookie"]) {
141 | res.headers["set-cookie"].forEach( function( cookie ) {
142 | props = cookie.split(";");
143 | var newcookie = {
144 | "value": "",
145 | "domain": "",
146 | "path": "/",
147 | "expires": ""
148 | };
149 |
150 | newcookie.value = props.shift();
151 | props.forEach( function( prop ) {
152 | var parts = prop.split("="),
153 | name = parts[0].trim();
154 | switch(name.toLowerCase()) {
155 | case "domain":
156 | newcookie.domain = parts[1].trim();
157 | break;
158 | case "path":
159 | newcookie.path = parts[1].trim();
160 | break;
161 | case "expires":
162 | newcookie.expires = parts[1].trim();
163 | break;
164 | }
165 | });
166 | if(newcookie.domain == "") newcookie.domain = curl.hostname;
167 | var match = cookies.filter(function(value, index, arr) {
168 | if(value.domain == newcookie.domain && value.path == newcookie.path && value.value.split("=")[0] == newcookie.value.split("=")[0]) {
169 | arr[index] = newcookie;
170 | return true;
171 | }
172 | else {
173 | return false;
174 | }
175 | });
176 | if(match.length == 0) cookies.push(newcookie);
177 | });
178 | }
179 | });
180 | }
181 |
182 | this.getCookie = function(domain, name) {
183 | var mycookies = cookies.filter(function(value, index, arr) {
184 | return(domain == value.domain);
185 | });
186 | for( var i=0; i < mycookies.length; i++ ) {
187 | parts = mycookies[i].value.split("=");
188 | if( parts[0] == name ) {
189 | return parts[1];
190 | }
191 | }
192 |
193 | return null;
194 | }
195 | }
196 | sys.inherits(httpclient, events.EventEmitter);
197 | exports.httpclient = httpclient;
198 |
--------------------------------------------------------------------------------
/dependencies/node-async-testing/async_testing.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var assert = require('assert');
3 |
4 | var AssertWrapper = exports.AssertWrapper = function(test) {
5 | this.__test = test;
6 | var assertion_functions = [
7 | 'ok',
8 | 'equal',
9 | 'notEqual',
10 | 'deepEqual',
11 | 'notDeepEqual',
12 | 'strictEqual',
13 | 'notStrictEqual',
14 | 'throws',
15 | 'doesNotThrow'
16 | ];
17 |
18 | assertion_functions.forEach(function(func_name) {
19 | this[func_name] = function() {
20 | try {
21 | assert[func_name].apply(null, arguments);
22 | this.__test.__numAssertions++;
23 | }
24 | catch(err) {
25 | if( err instanceof assert.AssertionError ) {
26 | this.__test.failed(err);
27 | }
28 | }
29 | }
30 | }, this);
31 | };
32 |
33 | var Test = function(name, func, suite) {
34 | this.assert = new AssertWrapper(this);
35 | this.numAssertionsExpected = null;
36 |
37 | this.__name = name;
38 | this.__func = func;
39 | this.__suite = suite;
40 | this.__promise = new process.Promise();
41 | this.__numAssertions = 0;
42 | this.__finished = false;
43 | this.__failure = null;
44 | this.__symbol = '.';
45 | };
46 | Test.prototype.run = function() {
47 | //sys.puts('Starting test "' + this.__name + '" ...');
48 | this.__func(this);
49 | };
50 | Test.prototype.finish = function() {
51 | if( !this.__finished ) {
52 | this.__finished = true;
53 |
54 | if( this.__failure === null && this.numAssertionsExpected !== null ) {
55 | try {
56 | var message = this.numAssertionsExpected + (this.numAssertionsExpected == 1 ? ' assertion was ' : ' assertions were ')
57 | + 'expected but only ' + this.__numAssertions + ' fired';
58 | assert.equal(this.numAssertionsExpected, this.__numAssertions, message);
59 | }
60 | catch(err) {
61 | this.__failure = err;
62 | this.__symbol = 'F';
63 | }
64 | }
65 |
66 | this.__promise.emitSuccess(this.__numAssertions);
67 | }
68 | };
69 | Test.prototype.failed = function(err) {
70 | if( !this.__finished ) {
71 | this.__failure = err;
72 | this.__symbol = 'F';
73 | this.finish();
74 | }
75 | };
76 |
77 | var tests = [];
78 | process.addListener('exit', function() {
79 | if( tests.length < 1 ) {
80 | return;
81 | }
82 |
83 | var failures = [];
84 | sys.error('\nResults:');
85 |
86 | var output = '';
87 | tests.forEach(function(t) {
88 | if( !t.__finished ) {
89 | t.finish();
90 | }
91 | if( t.__failure !== null ) {
92 | failures.push(t);
93 | }
94 |
95 | output += t.__symbol;
96 | });
97 |
98 | sys.error(output);
99 | failures.forEach(function(t) {
100 | sys.error('');
101 |
102 | sys.error('test "' + t.__name + '" failed: ');
103 | sys.error(t.__failure.stack || t.__failure);
104 | });
105 |
106 | sys.error('');
107 | });
108 |
109 | var test = exports.test = function(name, func) {
110 | var t = new Test(name, func);
111 | tests.push(t);
112 |
113 | t.run();
114 | };
115 |
116 | var TestSuite = exports.TestSuite = function(name) {
117 | this.name = name;
118 | this.wait = false;
119 | this.tests = [];
120 | this.numAssertions = 0;
121 | this.numFinishedTests = 0;
122 | this.finished = false;
123 |
124 | this._setup = null;
125 | this._teardown = null;
126 |
127 | var suite = this;
128 | process.addListener('exit', function() {
129 | suite.finish();
130 | });
131 | };
132 | TestSuite.prototype.finish = function() {
133 | if( this.finished ) {
134 | return;
135 | }
136 |
137 | this.finished = true;
138 |
139 | sys.error('\nResults for ' + (this.name ? '"' + (this.name || '')+ '"' : 'unnamed suite') + ':');
140 | var failures = [];
141 | var output = '';
142 | this.tests.forEach(function(t) {
143 | if( !t.__finished ) {
144 | t.finish();
145 | }
146 | if( t.__failure !== null ) {
147 | failures.push(t);
148 | }
149 | output += t.__symbol;
150 | });
151 |
152 | sys.error(output);
153 |
154 | output = this.tests.length + ' test' + (this.tests.length == 1 ? '' : 's') + '; ';
155 | output += failures.length + ' failure' + (failures.length == 1 ? '' : 's') + '; ';
156 | output += this.numAssertions + ' assertion' + (this.numAssertions == 1 ? '' : 's') + ' ';
157 | sys.error(output);
158 |
159 | failures.forEach(function(t) {
160 | sys.error('');
161 |
162 | sys.error('test "' + t.__name + '" failed: ');
163 | sys.error(t.__failure.stack || t.__failure);
164 | });
165 |
166 | sys.error('');
167 | };
168 |
169 | TestSuite.prototype.setup = function(func) {
170 | this._setup = func;
171 | return this;
172 | };
173 | TestSuite.prototype.teardown = function(func) {
174 | this._teardown = func;
175 | return this;
176 | };
177 | TestSuite.prototype.waitForTests = function(yesOrNo) {
178 | if(typeof yesOrNo == 'undefined') {
179 | yesOrNo = true;
180 | }
181 | this.wait = yesOrNo;
182 | return this;
183 | };
184 | TestSuite.prototype.runTests = function(tests) {
185 | //sys.puts('\n' + (this.name? '"' + (this.name || '')+ '"' : 'unnamed suite'));
186 | for( var testName in tests ) {
187 | var t = new Test(testName, tests[testName], this);
188 | this.tests.push(t);
189 | };
190 |
191 | this.runTest(0);
192 | };
193 | TestSuite.prototype.runTest = function(testIndex) {
194 | if( testIndex >= this.tests.length ) {
195 | return;
196 | }
197 |
198 | var t = this.tests[testIndex];
199 |
200 | if(this._setup) {
201 | this._setup.call(t,t);
202 | }
203 |
204 | var suite = this;
205 | var wait = suite.wait;
206 | t.__promise.addCallback(function(numAssertions) {
207 | if(suite._teardown) {
208 | suite._teardown.call(t,t);
209 | }
210 |
211 | suite.numAssertions += numAssertions;
212 | suite.numFinishedTests++;
213 |
214 | if( wait ) {
215 | suite.runTest(testIndex+1);
216 | }
217 |
218 | if( suite.numFinishedTests == suite.tests.length ) {
219 | suite.finish();
220 | }
221 | });
222 |
223 | t.run();
224 |
225 | if( !wait ) {
226 | suite.runTest(testIndex+1);
227 | }
228 |
229 | };
230 |
--------------------------------------------------------------------------------
/lib/promise.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2004-2009, The Dojo Foundation All Rights Reserved.
3 | Available via Academic Free License >= 2.1 OR the modified BSD license.
4 | see: http://dojotoolkit.org/license for details
5 |
6 | This is a modified version with no remaining dependencies to dojo.
7 | */
8 | var hitch = function() {
9 | var args = Array.prototype.slice.call(arguments);
10 | var method = args.shift();
11 | if (method instanceof Function) {
12 | var scope = null;
13 | }
14 | else {
15 | var scope = method;
16 | method = args.shift();
17 | }
18 |
19 | if (scope || args.length > 0) {
20 | return function() {
21 | return method.apply(
22 | scope,
23 | args.concat(Array.prototype.slice.call(arguments))
24 | );
25 | };
26 | }
27 | else {
28 | return method;
29 | }
30 | };
31 |
32 | exports.Promise = function(/*Function?*/ canceller) {
33 | this.chain = [];
34 | this.id = this._nextId();
35 | this.fired = -1;
36 | this.paused = 0;
37 | this.results = [null, null];
38 | this.canceller = canceller;
39 | this.silentlyCancelled = false;
40 | };
41 |
42 | process.mixin(exports.Promise.prototype, {
43 | _nextId: (function() {
44 | var n = 1;
45 | return function() { return n++; };
46 | })(),
47 |
48 | cancel: function() {
49 | var err;
50 | if (this.fired == -1) {
51 | if (this.canceller) {
52 | err = this.canceller(this);
53 | }
54 | else {
55 | this.silentlyCancelled = true;
56 | }
57 | if (this.fired == -1) {
58 | if (!(err instanceof Error)) {
59 | var res = err;
60 | var msg = "Promise Cancelled";
61 | if (err && err.toString) {
62 | msg += ": " + err.toString();
63 | }
64 | err = new Error(msg);
65 | err.cancelResult = res;
66 | }
67 | this.errback(err);
68 | }
69 | }
70 | else if ( (this.fired == 0) &&
71 | ( (this.results[0] instanceof promise.Promise) ||
72 | (this.results[0] instanceof process.Promise)
73 | ) ) {
74 | this.results[0].cancel();
75 | }
76 | },
77 |
78 |
79 | _resback: function(res) {
80 | // summary:
81 | // The private primitive that means either callback or errback
82 | this.fired = ((res instanceof Error) ? 1 : 0);
83 | this.results[this.fired] = res;
84 |
85 | this._fire();
86 | },
87 |
88 | _check: function() {
89 | if (this.fired != -1) {
90 | if (!this.silentlyCancelled) {
91 | throw new Error("already called!");
92 | }
93 | this.silentlyCancelled = false;
94 | return;
95 | }
96 | },
97 |
98 | callback: function(arg) {
99 | // summary:
100 | // Begin the callback sequence with a non-error value.
101 |
102 | /*
103 | callback or errback should only be called once on a given
104 | Promise.
105 | */
106 | this._check();
107 | this._resback(arg);
108 | },
109 |
110 | errback: function(/*Error*/res) {
111 | // summary:
112 | // Begin the callback sequence with an error result.
113 | this._check();
114 | if (!(res instanceof Error)) {
115 | res = new Error(res);
116 | }
117 | this._resback(res);
118 | },
119 |
120 | addBoth: function(/*Function|Object*/cb, /*String?*/cbfn) {
121 | // summary:
122 | // Add the same function as both a callback and an errback as the
123 | // next element on the callback sequence.This is useful for code
124 | // that you want to guarantee to run, e.g. a finalizer.
125 | var enclosed = hitch.apply(null, arguments);
126 | return this.then(enclosed, enclosed);
127 | },
128 |
129 | addCallback: function(/*Function|Object*/cb, /*String?*/cbfn /*...*/) {
130 | return this.then(hitch.apply(null, arguments));
131 | },
132 |
133 | addErrback: function(cb, cbfn) {
134 | // summary:
135 | // Add a single callback to the end of the callback sequence.
136 | return this.then(null, hitch.apply(null, arguments));
137 | },
138 |
139 | then: function(cb, eb) {
140 | // summary:
141 | // Add separate callback and errback to the end of the callback
142 | // sequence.
143 | this.chain.push([cb, eb])
144 | if (this.fired >= 0) {
145 | this._fire();
146 | }
147 | return this;
148 | },
149 |
150 | _fire: function() {
151 | // summary:
152 | // Used internally to exhaust the callback sequence when a result
153 | // is available.
154 | var chain = this.chain;
155 | var fired = this.fired;
156 | var res = this.results[fired];
157 | var self = this;
158 | var cb = null;
159 |
160 | while ( (chain.length > 0) &&
161 | (this.paused == 0) ) {
162 | // Array
163 | var f = chain.shift()[fired];
164 |
165 | if (!f) {
166 | continue;
167 | }
168 | var func = function() {
169 | var ret = f(res);
170 | //If no response, then use previous response.
171 | if (typeof ret != "undefined") {
172 | res = ret;
173 | }
174 |
175 | fired = ((res instanceof Error) ? 1 : 0);
176 |
177 | if ( (res instanceof exports.Promise) ||
178 | (res instanceof process.Promise) ) {
179 | cb = function(res) {
180 | self._resback(res);
181 | // inlined from _pause()
182 | self.paused--;
183 | if ( (self.paused == 0) &&
184 | (self.fired >= 0) ) {
185 | self._fire();
186 | }
187 | }
188 | // inlined from _unpause
189 | this.paused++;
190 | }
191 | };
192 |
193 | try{
194 | func.call(this);
195 | }catch(err) {
196 | fired = 1;
197 | res = err;
198 | }
199 | }
200 | if ( chain.length < 1 && fired == 1 && res ) {
201 | throw res;
202 | }
203 |
204 | this.fired = fired;
205 | this.results[fired] = res;
206 | if ((cb)&&(this.paused)) {
207 | // this is for "tail recursion" in case the dependent
208 | // promise is already fired
209 | if (res instanceof exports.Promise) {
210 | res.addBoth(cb);
211 | }
212 | else {
213 | res.addCallback(cb);
214 | res.addErrback(cb);
215 | }
216 | }
217 | }
218 | });
219 |
--------------------------------------------------------------------------------
/test/test-response.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var TestSuite = require('../dependencies/node-async-testing/async_testing').TestSuite;
3 | var path = require('path');
4 |
5 | var BomberResponse = require('../lib/response').Response;
6 | var MockResponse = require('./mocks/response').MockResponse;
7 |
8 | (new TestSuite('Response Tests'))
9 | .setup(function() {
10 | this.mr = new MockResponse();
11 | this.br = new BomberResponse(this.mr);
12 | })
13 | .runTests({
14 | "test simple": function(test) {
15 | test.br.send('Hi there');
16 |
17 | test.assert.equal(200, test.mr.status);
18 | test.assert.ok(test.mr.finished);
19 | test.assert.equal(1, test.mr.body.length);
20 | test.assert.equal('Hi there', test.mr.bodyText);
21 | },
22 | "test finish on send": function(test) {
23 | test.br.finishOnSend = false;
24 |
25 | test.br.send('Hi there');
26 | test.assert.ok(!test.mr.finished);
27 |
28 | test.br.send('Hello');
29 | test.assert.ok(!test.mr.finished);
30 |
31 | test.br.finish();
32 | test.assert.ok(test.mr.finished);
33 | },
34 | "test can't finish twice": function(test) {
35 | test.br.send('Hi there');
36 | test.assert.throws(function() {
37 | test.br.finish();
38 | });
39 | },
40 | "test can't send after finishing": function(test) {
41 | test.br.send('Hi there');
42 | test.assert.throws(function() {
43 | test.br.send('');
44 | });
45 | },
46 | "test can't send header twice": function(test) {
47 | test.br.sendHeaders();
48 | test.assert.throws(function() {
49 | test.br.sendHeaders();
50 | });
51 | },
52 | "test header isn't sent twice if manually sent": function(test) {
53 | //headers haven't been sent
54 | test.assert.ok(!test.mr.headers);
55 | test.br.sendHeaders();
56 |
57 | // now they have
58 | test.assert.ok(test.mr.headers);
59 |
60 | // no problem!
61 | test.assert.doesNotThrow(function() {
62 | test.br.send('hi there');
63 | });
64 | },
65 | "test Content-Type set automatically": function(test) {
66 | test.br.send('Hi there');
67 |
68 | test.assert.equal('text/html; charset=UTF-8', test.mr.headers['Content-Type']);
69 | },
70 | "test Content-Type set through variable": function(test) {
71 | test.br.mimeType = 'something';
72 | test.br.send('Hi there');
73 |
74 | test.assert.equal('something', test.mr.headers['Content-Type']);
75 | },
76 | "test Content-Type set through setHeader": function(test) {
77 | test.br.setHeader('Content-Type', 'something');
78 | test.br.send('Hi there');
79 |
80 | test.assert.equal('something', test.mr.headers['Content-Type']);
81 | },
82 | "test Content-Type set through headers": function(test) {
83 | test.br.headers['Content-Type'] = 'something';
84 | test.br.send('Hi there');
85 |
86 | test.assert.equal('something', test.mr.headers['Content-Type']);
87 | },
88 | "test Content-Type gets overriden by explicitly set header": function(test) {
89 | test.br.mimeType = 'text/something else';
90 | test.br.headers['Content-Type'] = 'something';
91 | test.br.send('Hi there');
92 |
93 | test.assert.equal('something', test.mr.headers['Content-Type']);
94 | },
95 | "test charset set automatically if known Content-Type": function(test) {
96 | test.br.mimeType = 'text/html';
97 | test.br.send('Hi there');
98 |
99 | test.assert.equal('text/html; charset=UTF-8', test.mr.headers['Content-Type']);
100 | },
101 | "test charset not set automatically if unknown Content-Type": function(test) {
102 | test.br.mimeType = 'unknown';
103 | test.br.send('Hi there');
104 |
105 | test.assert.equal('unknown', test.mr.headers['Content-Type']);
106 | },
107 | "test charset can be explicitly set": function(test) {
108 | test.br.mimeType = 'unknown';
109 | test.br.charset = 'CHARSET';
110 | test.br.send('Hi there');
111 |
112 | test.assert.equal('unknown; charset=CHARSET', test.mr.headers['Content-Type']);
113 | },
114 | "test status can be set": function(test) {
115 | test.br.status = 404;
116 | test.br.send('Hi there');
117 |
118 | test.assert.equal(404, test.mr.status);
119 | },
120 | "test send file": function(test) {
121 | test.br.sendFile(path.dirname(__filename)+'/fixtures/testApp/resources/image.png').wait();
122 |
123 | test.assert.equal(200, test.mr.status);
124 | test.assert.equal('this is a fake image\n', test.mr.bodyText);
125 |
126 | test.assert.ok(test.mr.finished);
127 | },
128 | "test send file doesn't exist": function(test) {
129 | test.assert.throws(function() {
130 | test.br.sendFile('non-existant').wait();
131 | });
132 | },
133 | "test send file will set Content-Type": function(test) {
134 | test.br.sendFile(path.dirname(__filename)+'/fixtures/testApp/resources/image.png').wait();
135 | test.assert.equal('image/png', test.mr.headers['Content-Type']);
136 | },
137 | "test send file will not override Content-Type": function(test) {
138 | test.br.mimeType = 'not/image';
139 | test.br.sendFile(path.dirname(__filename)+'/fixtures/testApp/resources/image.png').wait();
140 | test.assert.equal('not/image', test.mr.headers['Content-Type']);
141 | },
142 | "test send file with finishOnSend false": function(test) {
143 | test.br.finishOnSend = false;
144 |
145 | test.br.sendFile(path.dirname(__filename)+'/fixtures/testApp/resources/image.png').wait();
146 |
147 | test.assert.ok(!test.mr.finished);
148 | },
149 | "test send file with other sends": function(test) {
150 | test.br.finishOnSend = false;
151 |
152 | test.br.send('one');
153 | test.br.sendFile(path.dirname(__filename)+'/fixtures/testApp/resources/image.png').wait();
154 | test.br.send('two');
155 |
156 | test.assert.equal('onethis is a fake image\ntwo', test.mr.bodyText);
157 | },
158 | "test send file doesn't resend headers": function(test) {
159 | test.br.sendHeaders();
160 | test.assert.ok(test.mr.headers);
161 | test.mr.headers = null;
162 | test.assert.ok(!test.mr.headers);
163 | test.br.sendFile(path.dirname(__filename)+'/fixtures/testApp/resources/image.png').wait();
164 | test.assert.ok(!test.mr.headers);
165 | },
166 | "test send file resets encoding after it is finished": function(test) {
167 | test.br.sendFile(path.dirname(__filename)+'/fixtures/testApp/resources/image.png').wait();
168 | test.assert.equal(BomberResponse.__defaultEncoding, test.br.encoding);
169 | },
170 | });
171 |
--------------------------------------------------------------------------------
/test/test-router.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var TestSuite = require('../dependencies/node-async-testing/async_testing').TestSuite;
3 | var path = require('path');
4 |
5 | var Router = require('../lib/router').Router;
6 |
7 | var BomberResponse = require('../lib/response').Response;
8 | var BomberRequest = require('../lib/request').Request;
9 |
10 | var MockRequest = require('./mocks/request').MockRequest;
11 | var MockResponse = require('./mocks/response').MockResponse;
12 |
13 |
14 | // Simple routes
15 | (new TestSuite('Router Tests'))
16 | .setup(function() {
17 | this.r = new Router();
18 | this.r.path = path.dirname(__filename)+'/fixtures/testApp/routes.js';
19 | })
20 | .runTests({
21 | "test router.add": function(test) {
22 | test.r.add('/(a)(b)(c)', { view: 'view_name', action: 'action_name' });
23 | test.r.add('/defer', "subApp");
24 | test.r.add('/:action/:id/:more', { view: 'view_name' });
25 | test.r.add('/:action/:id', { view: 'view_name_int', id: '[0-9]+' });
26 | test.r.add('/:action/:id', { view: 'view_name' });
27 | test.r.add('/:action', { view: 'view_name' });
28 | test.r.add('/', { view: 'view_name', action: 'action_name' });
29 |
30 | var tests = [
31 | [ ['GET', '/'],
32 | { action: {
33 | view: 'view_name',
34 | action: 'action_name'
35 | },
36 | params: {
37 | }
38 | } ],
39 | [ ['GET', '/abc'],
40 | { action: {
41 | view: 'view_name',
42 | action: 'action_name'
43 | },
44 | params: {
45 | args: ['a','b','c']
46 | }
47 | } ],
48 | [ ['GET', '/action_name'],
49 | { action: {
50 | view: 'view_name',
51 | action: 'action_name'
52 | },
53 | params: {
54 | }
55 | } ],
56 | [ ['GET', '/action_name/1/another'],
57 | { action: {
58 | view: 'view_name',
59 | action: 'action_name'
60 | },
61 | params: {
62 | id: "1",
63 | more: "another"
64 | }
65 | } ],
66 | [ ['GET', '/action_name/1'],
67 | { action: {
68 | view: 'view_name_int',
69 | action: 'action_name'
70 | },
71 | params: {
72 | id: "1"
73 | }
74 | } ],
75 | [ ['GET', '/action_name/string'],
76 | { action: {
77 | view: 'view_name',
78 | action: 'action_name'
79 | },
80 | params: {
81 | id: "string"
82 | }
83 | } ],
84 | [ ['GET', '/action_name/string/'],
85 | { action: {
86 | view: 'view_name',
87 | action: 'action_name'
88 | },
89 | params: {
90 | id: "string"
91 | }
92 | } ],
93 | [ ['GET', '/defer/more/path'],
94 | { action: {
95 | app: 'subApp',
96 | },
97 | params: {},
98 | path: '/more/path'
99 | } ],
100 | ];
101 |
102 |
103 | tests.forEach(function(t) {
104 | var route = test.r.findRoute.apply(test.r, t[0]);
105 | test.assert.deepEqual(t[1], route);
106 | });
107 | },
108 | "test addFolder adds route": function(test) {
109 | test.r.addFolder();
110 | test.assert.equal(1, test.r._routes.length);
111 | },
112 | "test addFolder adds route with no path specified": function(test) {
113 | test.r.addFolder();
114 |
115 | var route = test.r.findRoute('GET','/resources/file.txt');
116 | test.assert.ok(route.action.action);
117 | },
118 | "test addFolder adds route with path specified": function(test) {
119 | test.r.addFolder({path: '/media/'});
120 |
121 | var route = test.r.findRoute('GET','/media/file.txt');
122 | test.assert.ok(route.action.action);
123 | },
124 | "test addFolder adds route with no folder specified": function(test) {
125 | test.r.addFolder();
126 |
127 | var route = test.r.findRoute('GET','/resources/file.txt');
128 | test.assert.equal(route.params.folder, './resources/');
129 | },
130 | "test addFolder adds route with folder specified": function(test) {
131 | test.r.addFolder({folder: '/path/to/folder/'});
132 |
133 | var route = test.r.findRoute('GET','/resources/file.txt');
134 | test.assert.equal(route.params.folder, '/path/to/folder/');
135 | },
136 | "test route gets filename correctly": function(test) {
137 | test.r.addFolder({folder: '/path/to/folder/'});
138 |
139 | var route = test.r.findRoute('GET','/resources/file.txt');
140 | test.assert.equal(route.params.filename, 'file.txt');
141 | },
142 | "test generate function serves file": function(test) {
143 | test.r.addFolder();
144 |
145 | var url = '/resources/file.txt';
146 |
147 | var route = test.r.findRoute('GET',url);
148 |
149 | var mrequest = new MockRequest('GET', url);
150 | var mresponse = new MockResponse();
151 | var request = new BomberRequest(mrequest, {"href": url, "pathname": url}, route);
152 | var response = new BomberResponse(mresponse);
153 |
154 | route.action.action(request, response).wait();
155 |
156 | test.assert.equal(200, mresponse.status);
157 | test.assert.equal('text\n', mresponse.bodyText);
158 | },
159 | "test generate function serves file from absolute folder": function(test) {
160 | test.r.addFolder({folder: path.dirname(__filename)+'/fixtures/testApp/resources/'});
161 |
162 | var url = '/resources/file.txt';
163 |
164 | var route = test.r.findRoute('GET',url);
165 |
166 | var mrequest = new MockRequest('GET', url);
167 | var mresponse = new MockResponse();
168 | var request = new BomberRequest(mrequest, {"href": url, "pathname": url}, route);
169 | var response = new BomberResponse(mresponse);
170 |
171 | route.action.action(request, response).wait();
172 |
173 | test.assert.equal(200, mresponse.status);
174 | },
175 | "test returns 404 for non-existant file": function(test) {
176 | test.r.addFolder({folder: path.dirname(__filename)+'/fixtures/testApp/resources/'});
177 |
178 | var url = '/resources/non-existant';
179 |
180 | var route = test.r.findRoute('GET',url);
181 |
182 | var mrequest = new MockRequest('GET', url);
183 | var mresponse = new MockResponse();
184 | var request = new BomberRequest(mrequest, {"href": url, "pathname": url}, route);
185 | var response = new BomberResponse(mresponse);
186 |
187 | var http_response = route.action.action(request, response).wait();
188 |
189 | test.assert.equal('HTTP404NotFound', http_response.name);
190 | },
191 | });
192 |
--------------------------------------------------------------------------------
/test/test-cookies.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys'),
2 | path = require('path');
3 |
4 | var TestSuite = require('../dependencies/node-async-testing/async_testing').TestSuite,
5 | httpclient = require('../dependencies/node-httpclient/lib/httpclient');
6 |
7 | // the testing apps assume that bomberjs is on the path.
8 | require.paths.push(path.dirname(__filename)+'/../..');
9 | var BomberServer = require('bomberjs/lib/server').Server;
10 | var App = require('bomberjs/lib/app').App;
11 |
12 | (new TestSuite('Cookie Tests -- over HTTP'))
13 | .setup(function() {
14 | this.project = {config:{}};
15 | this.project.base_app = new App('bomberjs/test/fixtures/testApp', this.project);
16 | this.server = new BomberServer(this.project);
17 | this.server.start();
18 |
19 | this.url_base = 'http://localhost:'+this.project.config.server.port+'/cookie-tests/';
20 |
21 | this.client = new httpclient.httpclient();
22 | })
23 | .teardown(function() {
24 | this.server.stop();
25 | })
26 | .waitForTests()
27 | .runTests({
28 | "test set cookie": function(test) {
29 | test.client.perform(test.url_base+'set?name1=value1&name2=value2', "GET", function(result) {
30 | // client reads them fine
31 | test.assert.equal('value1', test.client.getCookie('localhost','name1'));
32 | test.assert.equal('value2', test.client.getCookie('localhost','name2'));
33 |
34 | // the action can immediately read them after setting them
35 | test.assert.equal('value1,value2',result.response.body);
36 | test.finish();
37 | }, null);
38 | },
39 | "test read cookie": function(test) {
40 | test.client.perform(test.url_base+'read?name', "GET", function(result) {
41 | //cookie doesn't exist
42 | test.assert.equal('', result.response.body);
43 | //set it
44 | test.client.perform(test.url_base+'set?name=value', "GET", function(result) {
45 | // now read it out
46 | test.client.perform(test.url_base+'read?name', "GET", function(result) {
47 | test.assert.equal('value', result.response.body);
48 | test.finish();
49 | }, null);
50 | }, null);
51 | }, null);
52 | },
53 | "test read cookie default value": function(test) {
54 | test.client.perform(test.url_base+'read?name&_default=4', "GET", function(result) {
55 | test.assert.equal('4', result.response.body);
56 | test.finish();
57 | }, null);
58 | },
59 | "test set secure cookie": function(test) {
60 | test.client.perform(test.url_base+'setSecure?name1=value1&name2=value2', "GET", function(result) {
61 | // can't read it in client
62 | test.assert.notEqual('value1', test.client.getCookie('localhost','name1'));
63 | test.assert.notEqual('value2', test.client.getCookie('localhost','name2'));
64 | test.finish();
65 | }, null);
66 | },
67 | "test read secure cookie": function(test) {
68 | test.client.perform(test.url_base+'readSecure?name', "GET", function(result) {
69 | //cookie doesn't exist
70 | test.assert.equal('', result.response.body);
71 | //set it
72 | test.client.perform(test.url_base+'setSecure?name=value', "GET", function(result) {
73 | // can't read it normally
74 | test.client.perform(test.url_base+'read?name', "GET", function(result) {
75 | test.assert.notEqual('value', result.response.body);
76 | // can read it using secure function
77 | test.client.perform(test.url_base+'readSecure?name', "GET", function(result) {
78 | test.assert.equal('value', result.response.body);
79 | test.finish();
80 | }, null);
81 | }, null);
82 | }, null);
83 | }, null);
84 | },
85 | "test read secure cookie default value": function(test) {
86 | test.client.perform(test.url_base+'readSecure?name&_default=4', "GET", function(result) {
87 | test.assert.equal('4', result.response.body);
88 | test.finish();
89 | }, null);
90 | },
91 | "test read secure cookie with mangled secret": function(test) {
92 | test.client.perform(test.url_base+'setSecure?name=value', "GET", function(result) {
93 | test.project.config.security.signing_secret = 'mangled';
94 | test.client.perform(test.url_base+'readSecure?name', "GET", function(result) {
95 | test.assert.equal('', result.response.body);
96 | test.finish();
97 | }, null);
98 | }, null);
99 | },
100 | "test unset cookie": function(test) {
101 | // first set the cookies
102 | test.client.perform(test.url_base+'set?name1=value1&name2=value2', "GET", function(result) {
103 | // now unset one
104 | test.client.perform(test.url_base+'unset?name2', "GET", function(result) {
105 | // the client no longer has the cookie
106 | test.assert.equal('value1', test.client.getCookie('localhost','name1'));
107 | test.assert.equal('', test.client.getCookie('localhost','name2'));
108 |
109 | // if the action tries to read the unset cookie it gets nothing
110 | test.assert.equal('', result.response.body);
111 | test.finish();
112 | }, null);
113 | }, null);
114 | },
115 | "test keys()": function(test) {
116 | // first set the cookies
117 | test.client.perform(test.url_base+'set?session_key=1&name1=value1&name2=value2', "GET", function(result) {
118 | // now read the keys
119 | test.client.perform(test.url_base+'keys', "GET", function(result) {
120 | test.assert.equal('session_key,name1,name2', result.response.body);
121 | test.finish();
122 | }, null);
123 | }, null);
124 | },
125 | "test keys()": function(test) {
126 | // first set the cookies
127 | test.client.perform(test.url_base+'set?name1=value1', "GET", function(result) {
128 | // now read the keys
129 | test.client.perform(test.url_base+'exists?name1&name2', "GET", function(result) {
130 | test.assert.equal('1,0', result.response.body);
131 | test.finish();
132 | }, null);
133 | }, null);
134 | },
135 | "test reset cookies": function(test) {
136 | //set cookie
137 | test.client.perform(test.url_base+'set?name1=value1&name2=value2', "GET", function(result) {
138 | // now reset them
139 | test.client.perform(test.url_base+'reset', "GET", function(result) {
140 | test.assert.equal('', test.client.getCookie('localhost','name1'));
141 | test.assert.equal('', test.client.getCookie('localhost','name2'));
142 |
143 | test.client.perform(test.url_base+'read?name1&name2', "GET", function(result) {
144 | test.assert.equal('', result.response.body);
145 | test.finish();
146 | }, null);
147 | }, null);
148 | }, null);
149 | },
150 | });
151 |
152 |
--------------------------------------------------------------------------------
/website/assets/style.css:
--------------------------------------------------------------------------------
1 | /* give HTML5 block-level elements 'display: block;' */
2 | article , header , footer , section, video { display: block; }
3 |
4 | a { color: #008; }
5 | a:visited { color: #00c; }
6 | a:hover { color: #005; }
7 |
8 | abbr { border-bottom: 1px dotted; }
9 |
10 | body {
11 | background: #EEE;
12 | color: #222;
13 | font-family: 'Helvetica', sans-serif;
14 | line-height: 1.4em;
15 | margin: 0; padding: 0 0 4.2em;
16 | }
17 |
18 | p, ul, ol, blockquote, dl { margin-top: 1.4em; margin-bottom: 1.4em; }
19 | ul, ol { margin-left: 0; padding-left: 30px; }
20 |
21 | footer { text-align: right; }
22 |
23 | div.warning {
24 | background-color: #ff9999;
25 | border: 1px solid #660000;
26 | padding: 5px;
27 | margin: 0 20px;
28 | }
29 |
30 | #top {
31 | max-width: 700px;
32 | margin: 0;
33 | margin-left: 200px;
34 | margin-top: 140px;
35 | padding: 0 .66666em;
36 | }
37 | #top h1 {
38 | margin: 0 0 10px;
39 | }
40 | #top h1 a {
41 | color: #AAA;
42 | font-size: 3.1em;
43 | text-decoration: none;
44 | -webkit-text-stroke: 1px #222;
45 | }
46 | #top h1 a:hover {
47 | color: #888;
48 | }
49 | #top h2 {
50 | font-size: 1.5em;
51 | font-weight: normal;
52 | left: 5px;
53 | position: relative;
54 | margin: 0;
55 | }
56 |
57 | nav {
58 | float: left;
59 | font-size: 1.2em;
60 | left: 0;
61 | position: fixed;
62 | width: 195px;
63 | }
64 | nav#main {
65 | background: url(logo.png) no-repeat 100% 20px;
66 | padding-top: 260px;
67 | top: 0;
68 | }
69 | nav ul {
70 | line-height: 1.4em;
71 | list-style-type: none;
72 | margin: 0;
73 | padding: 0 15px 0 0;
74 | text-align: right;
75 | }
76 | nav ul ul {
77 | font-size: .9em;
78 | margin: 5px 0;
79 | }
80 |
81 | section , article {
82 | max-width: 700px;
83 | padding: 0 1em;
84 | margin-left: 200px;
85 | margin-top: 2.8em;
86 | }
87 | section article { padding: 0; }
88 |
89 | section header , article header { margin: 1.4em 0; }
90 | section header h1 , article header h1 { font-size: 1.4em; margin: 0; }
91 | section h1 , article h1 { font-size: 1.4em; margin: 0; }
92 | .recent_posts article h1, section h2 , article h2 { font-size: 1.2em; font-weight: normal; margin: .1.4em 0 -.7em; }
93 | section h3 , article h3 { font-size: 1em; font-weight: bold; margin: 0; }
94 | section header p , article header p { color: #666; font-size: .9em; margin: 0; }
95 |
96 | #post header h1 a {
97 | color: #333;
98 | text-decoration: none;
99 | }
100 | #post header h1 a:hover {
101 | color: #005;
102 | }
103 |
104 | .timeline dt { color: #666; display: block; float: left; font-size: .8em; font-weight: bold; padding-right: 1.2em; text-align: right; text-transform: uppercase; width: 65px; }
105 | .timeline dd { margin-left: 65px; padding-left: 1.2em; }
106 |
107 | .recent_posts article { position: relative; }
108 | .recent_posts article h1 { margin-right: 4.5em; }
109 | .recent_posts .meta { color: #666; display: inline; font-size: .8em; font-weight: bold; margin: 0; padding: 0; position: absolute; right: 0; text-transform: uppercase; top: 0; }
110 |
111 | table { width: 83%; }
112 | tbody th { text-align: right; }
113 | td { text-align: center; }
114 |
115 | .footnotes { border-top: 1px solid #666; font-size: .9em; }
116 | .footnotes p, .footnotes ul, .footnotes ol, .footnotes blockquote, .footnotes dl { margin-top: .7em; margin-bottom: .7em; }
117 | .footnotes ul, .footnotes ol { margin-left: 0; padding-left: 20px; }
118 | .footnotes hr { display: none; }
119 | .footnotes a[rev=footnote] { margin-left: 10px; font-size: .9em; }
120 |
121 | .highlight { background-color: #CCC; }
122 | .highlight pre { padding: .7em 1em; overflow: auto; }
123 |
124 | .highlight .c { color: #999988; font-style: italic } /* Comment */
125 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
126 | .highlight .k { font-weight: bold } /* Keyword */
127 | .highlight .o { font-weight: bold } /* Operator */
128 | .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
129 | .highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
130 | .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
131 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
132 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
133 | .highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
134 | .highlight .ge { font-style: italic } /* Generic.Emph */
135 | .highlight .gr { color: #aa0000 } /* Generic.Error */
136 | .highlight .gh { color: #999999 } /* Generic.Heading */
137 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
138 | .highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
139 | .highlight .go { color: #888888 } /* Generic.Output */
140 | .highlight .gp { color: #555555 } /* Generic.Prompt */
141 | .highlight .gs { font-weight: bold } /* Generic.Strong */
142 | .highlight .gu { color: #aaaaaa } /* Generic.Subheading */
143 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */
144 | .highlight .kc { font-weight: bold } /* Keyword.Constant */
145 | .highlight .kd { font-weight: bold } /* Keyword.Declaration */
146 | .highlight .kp { font-weight: bold } /* Keyword.Pseudo */
147 | .highlight .kr { font-weight: bold } /* Keyword.Reserved */
148 | .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
149 | .highlight .m { color: #009999 } /* Literal.Number */
150 | .highlight .s { color: #d14 } /* Literal.String */
151 | .highlight .na { color: #008080 } /* Name.Attribute */
152 | .highlight .nb { color: #0086B3 } /* Name.Builtin */
153 | .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
154 | .highlight .no { color: #008080 } /* Name.Constant */
155 | .highlight .ni { color: #800080 } /* Name.Entity */
156 | .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
157 | .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
158 | .highlight .nn { color: #555555 } /* Name.Namespace */
159 | .highlight .nt { color: #000080 } /* Name.Tag */
160 | .highlight .nv { color: #008080 } /* Name.Variable */
161 | .highlight .ow { font-weight: bold } /* Operator.Word */
162 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */
163 | .highlight .mf { color: #009999 } /* Literal.Number.Float */
164 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */
165 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */
166 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */
167 | .highlight .sb { color: #d14 } /* Literal.String.Backtick */
168 | .highlight .sc { color: #d14 } /* Literal.String.Char */
169 | .highlight .sd { color: #d14 } /* Literal.String.Doc */
170 | .highlight .s2 { color: #d14 } /* Literal.String.Double */
171 | .highlight .se { color: #d14 } /* Literal.String.Escape */
172 | .highlight .sh { color: #d14 } /* Literal.String.Heredoc */
173 | .highlight .si { color: #d14 } /* Literal.String.Interpol */
174 | .highlight .sx { color: #d14 } /* Literal.String.Other */
175 | .highlight .sr { color: #009926 } /* Literal.String.Regex */
176 | .highlight .s1 { color: #d14 } /* Literal.String.Single */
177 | .highlight .ss { color: #990073 } /* Literal.String.Symbol */
178 | .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
179 | .highlight .vc { color: #008080 } /* Name.Variable.Class */
180 | .highlight .vg { color: #008080 } /* Name.Variable.Global */
181 | .highlight .vi { color: #008080 } /* Name.Variable.Instance */
182 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */
183 |
--------------------------------------------------------------------------------
/test/test-action.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var TestSuite = require('../dependencies/node-async-testing/async_testing').TestSuite;
3 | var Promise = require('../lib/promise').Promise;
4 |
5 | var BomberResponse = require('../lib/response').Response;
6 | var BomberRequest = require('../lib/request').Request;
7 | var processAction = require('../lib/action').processAction;
8 |
9 | var MockRequest = require('./mocks/request').MockRequest;
10 | var MockResponse = require('./mocks/response').MockResponse;
11 |
12 | (new TestSuite('Action Tests')).runTests({
13 | "test return string": function(test) {
14 | var mrequest = new MockRequest('GET', '/');
15 | var mresponse = new MockResponse();
16 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
17 | var response = new BomberResponse(mresponse);
18 |
19 | var action = function(req, res) {
20 | return "hi";
21 | }
22 | processAction(request, response, action);
23 |
24 | test.assert.equal(200, mresponse.status);
25 | test.assert.equal('text/html; charset=UTF-8', mresponse.headers['Content-Type']);
26 | test.assert.equal('hi', mresponse.bodyText);
27 | },
28 | "test return object": function(test) {
29 | var mrequest = new MockRequest('GET', '/');
30 | var mresponse = new MockResponse();
31 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
32 | var response = new BomberResponse(mresponse);
33 |
34 | var obj = { a: 1, b: 2 };
35 | var action = function(req, res) {
36 | return obj;
37 | }
38 | processAction(request, response, action);
39 |
40 | test.assert.equal(200, mresponse.status);
41 | test.assert.equal('application/json', mresponse.headers['Content-Type']);
42 | test.assert.deepEqual(obj, JSON.parse(mresponse.bodyText));
43 | },
44 | "test return http response": function(test) {
45 | var mrequest = new MockRequest('GET', '/');
46 | var mresponse = new MockResponse();
47 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
48 | var response = new BomberResponse(mresponse);
49 |
50 | var action = function(req, res) {
51 | return new res.build.HTTP301MovedPermanently('http://google.com');
52 | }
53 | processAction(request, response, action);
54 |
55 | test.assert.equal(301, mresponse.status);
56 | test.assert.equal('', mresponse.bodyText);
57 | },
58 | "test return promise": function(test) {
59 | var mrequest = new MockRequest('GET', '/');
60 | var mresponse = new MockResponse();
61 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
62 | var response = new BomberResponse(mresponse);
63 |
64 | var promise = new Promise();
65 | var action = function(req, res) {
66 | return promise;
67 | }
68 | processAction(request, response, action);
69 |
70 | promise.callback('hey');
71 |
72 | test.assert.equal(200, mresponse.status);
73 | test.assert.equal('hey', mresponse.bodyText);
74 | },
75 | "test throw http response": function(test) {
76 | var mrequest = new MockRequest('GET', '/');
77 | var mresponse = new MockResponse();
78 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
79 | var response = new BomberResponse(mresponse);
80 |
81 | var action = function(req, res) {
82 | throw new res.build.redirect('http://google.com');
83 | }
84 | processAction(request, response, action);
85 |
86 | test.assert.equal(301, mresponse.status);
87 | test.assert.equal('http://google.com', mresponse.headers['Location']);
88 | test.assert.equal('', mresponse.bodyText);
89 | },
90 | "test return promise return response": function(test) {
91 | var mrequest = new MockRequest('GET', '/');
92 | var mresponse = new MockResponse();
93 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
94 | var response = new BomberResponse(mresponse);
95 |
96 | var promise = new Promise();
97 | var action = function(req, res) {
98 | promise.addCallback(function(arg) {
99 | return new res.build.redirect('http://google.com',303);
100 | });
101 | return promise;
102 | }
103 | processAction(request, response, action);
104 |
105 | promise.callback('hey');
106 |
107 | test.assert.equal(303, mresponse.status);
108 | test.assert.equal('http://google.com', mresponse.headers['Location']);
109 | test.assert.equal('', mresponse.bodyText);
110 | },
111 | "test return promise throw response": function(test) {
112 | var mrequest = new MockRequest('GET', '/');
113 | var mresponse = new MockResponse();
114 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
115 | var response = new BomberResponse(mresponse);
116 |
117 | var promise = new Promise();
118 | var gotHere1 = false;
119 | var gotHere2 = false;
120 | var didntGetHere = true;
121 | var action = function(req, res) {
122 | promise.addCallback(function(arg) {
123 | gotHere1 = true;
124 | return new res.build.redirect('http://google.com',303);
125 | });
126 | promise.addCallback(function(arg) {
127 | gotHere2 = true;
128 | throw new res.build.redirect('http://www.google.com',301);
129 | });
130 | promise.addCallback(function(arg) {
131 | didntGetHere = false;
132 | return "hi";
133 | });
134 | return promise;
135 | }
136 | processAction(request, response, action);
137 |
138 | promise.callback('hey');
139 |
140 | test.assert.ok(gotHere1);
141 | test.assert.ok(gotHere2);
142 | test.assert.ok(didntGetHere);
143 |
144 | test.assert.equal(301, mresponse.status);
145 | test.assert.equal('http://www.google.com', mresponse.headers['Location']);
146 | test.assert.equal('', mresponse.bodyText);
147 | },
148 | "test throw error": function(test) {
149 | var mrequest = new MockRequest('GET', '/');
150 | var mresponse = new MockResponse();
151 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
152 | var response = new BomberResponse(mresponse);
153 |
154 | var action = function(req, res) {
155 | throw new Error();
156 | }
157 | processAction(request, response, action);
158 |
159 | test.assert.equal(500, mresponse.status);
160 | },
161 | "test throw error from promise": function(test) {
162 | var mrequest = new MockRequest('GET', '/');
163 | var mresponse = new MockResponse();
164 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
165 | var response = new BomberResponse(mresponse);
166 |
167 | var promise = new Promise();
168 | var action = function(req, res) {
169 | promise.addCallback(function(arg) {
170 | throw new Error();
171 | });
172 | return promise;
173 | }
174 | processAction(request, response, action);
175 |
176 | promise.callback('hey');
177 |
178 | test.assert.equal(500, mresponse.status);
179 | },
180 | "test httpresponse": function(test) {
181 | var mrequest = new MockRequest('GET', '/');
182 | var mresponse = new MockResponse();
183 | var request = new BomberRequest(mrequest, {"href": "/", "pathname": "/"}, {});
184 | var response = new BomberResponse(mresponse);
185 |
186 | var action = function(req, res) {
187 | var r = new res.build.HTTPResponse('response');
188 | r.status = 101;
189 | r.mimeType = 'text';
190 | return r;
191 | }
192 | processAction(request, response, action);
193 |
194 | test.assert.equal(101, mresponse.status);
195 | test.assert.equal('text', mresponse.headers['Content-Type']);
196 | test.assert.equal('response', mresponse.bodyText);
197 | },
198 | });
199 |
--------------------------------------------------------------------------------
/lib/router.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys'),
2 | path = require('path');
3 |
4 | /* Router()
5 | *
6 | * The job of the router is to turn a url into a set of request parameters
7 | *
8 | * It does this by analyzing 'routes' that have been added to it.
9 | */
10 | var Router = exports.Router = function() {
11 | this._routes = [];
12 | };
13 |
14 | /* Router.regExpEscape(str)
15 | *
16 | * A function for escaping a string for turning it into a regular expression
17 | *
18 | * Parameters:
19 | *
20 | * + `str`: `String`. The string to escape for using in a regualr expression.
21 | *
22 | * Returns:
23 | *
24 | * An escaped string.
25 | */
26 | Router.regExpEscape = function(str) {
27 | return str.replace(/(\/|\.)/g, "\\$1");
28 | };
29 |
30 | /* Router.prototype.add(path, params_or_subApp)
31 | *
32 | * This adds a route to this particular Router.
33 | *
34 | * At its simplest a route is an object that has a) a regular expression to
35 | * match against a url and b) a set of instructions for what to do when a
36 | * match is found.
37 | *
38 | * Parameters:
39 | *
40 | * + `path`: `String` or `Regexp`. If it is a `RegExp` then nothing is changed.
41 | * If it is a `String` the string is converted to a regular expression.
42 | * + `params_or_subApp`: `Object` or `String`. If it is an `Object` then the
43 | * object is used to add constraints to the path, or used to fill in blanks
44 | * like app, view or action name. If it is a `String` then it should be the
45 | * name of a subApp that should handle the rest of the URL not matched.
46 | */
47 | Router.prototype.add = function(path, params_or_subApp) {
48 | if( typeof params_or_subApp != 'undefined' &&
49 | params_or_subApp !== null &&
50 | params_or_subApp.constructor == String ) {
51 | // it must be the name of a sub app
52 | var subApp = params_or_subApp;
53 | var params = {};
54 | }
55 | else {
56 | var subApp = null;
57 | var params = params_or_subApp || {};
58 | }
59 |
60 | if( !(path instanceof RegExp) ) {
61 | path = '^' + path;
62 |
63 | //if this doesn't route to a subapp then we want to match the whole path
64 | //so add end of line characters.
65 | if( subApp === null ) {
66 | if( path.lastIndexOf('/') != (path.length-1) ) {
67 | path = path + '/?';
68 | }
69 | path = path + '$';
70 | }
71 | path = Router.regExpEscape(path);
72 | var keys = [];
73 | path = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, function(str, p1) {
74 | keys.push(p1);
75 |
76 | if( params[p1] ) {
77 | //TODO, require that params[p1] be a regex in order to do this?
78 | return Router.regExpEscape('('+params[p1]+')');
79 | }
80 | else {
81 | return "([^\\\/.]+)";
82 | }
83 | });
84 | path = new RegExp(path);
85 | }
86 |
87 | var r = {
88 | method: null,
89 | regex: path,
90 | keys: keys,
91 | params: params,
92 | subApp: subApp
93 | };
94 |
95 | this._routes.push(r);
96 | };
97 |
98 |
99 | /* Router.prototype.addFolder(params)
100 | *
101 | * Add a route to server flat files in a folder. Defaults to serving content
102 | * ./app/resources/ from /resources/.
103 | *
104 | * Syntax:
105 | * var r = new Router();
106 | * r.addFolder();
107 | * r.addFolder({path:'/photos/'});
108 | * r.addFolder({path:'/custom/', folder:'custom'});
109 | */
110 | Router.prototype.addFolder = function(params) {
111 | // Prepare parameter defaults
112 | params = params || {};
113 | if ( !('path' in params) ) params.path = '/resources/';
114 | if ( !('folder' in params) ) params.folder = './resources/';
115 |
116 | // And append the route
117 | var router = this;
118 | var addFolderAction = function(request, response) {
119 | // Make sure there's not access to the parent folder
120 | if( request.params.filename.indexOf('..') >= 0 || request.params.filename.indexOf('./') >= 0 ) {
121 | return new response.build.forbidden();
122 | }
123 |
124 | // Resolve file
125 | var filename = path.join(request.params.folder, request.params.filename);
126 | if( request.params.folder.indexOf('/') !== 0 ) {
127 | filename = path.join(path.dirname(router.path), filename);
128 | }
129 |
130 | // Check that file exists and return
131 | var p = new process.Promise();
132 | response.sendFile(filename)
133 | .addErrback(function(err) {
134 | if( err.message == "No such file or directory" ) {
135 | p.emitSuccess( new response.build.notFound() );
136 | }
137 | else if( err.message == "Permission denied" ) {
138 | p.emitSuccess( new response.build.forbidden() );
139 | }
140 | return err;
141 | })
142 | .addCallback(function() {
143 | p.emitSuccess();
144 | });
145 | return p;
146 | };
147 |
148 | this.add(params.path + ':filename', {folder: params.folder, filename :'[^\\\\]+', action: addFolderAction});
149 | };
150 |
151 | /* Router.prototype.findRoute
152 | *
153 | * Searches through the list of routes that have been added to this router
154 | * and checks them against the passed in path_or_route.
155 | *
156 | * if path_or_route is
157 | * a string it is a path,
158 | * otherwise it is a route.
159 | *
160 | * It can take either because findRoute can sometimes return partial routes.
161 | * i.e. routes that have matched a part of the path but have to be passed along
162 | * to other apps for more processing.
163 | *
164 | * Partial routes are distinguished from regular routes by the existence of
165 | * a path key. For example:
166 | * {
167 | * path: "/rest/of/path",
168 | * action: { app: "subApp" }
169 | * }
170 | */
171 | Router.prototype.findRoute = function(method, path_or_route) {
172 | //TODO: configuration settings for ?
173 | // .html, .json, .xml format indicators
174 | // whether or not the end slash is allowed, disallowed or optional
175 |
176 | var numRoutes = this._routes.length;
177 |
178 | if( path_or_route.constructor == String ) { // it is a path
179 | var path = path_or_route;
180 | var route = {
181 | action: {},
182 | params: {},
183 | };
184 | }
185 | else { // it is a route object
186 | var route = path_or_route;
187 | var path = route.path;
188 | delete route.path;
189 | }
190 |
191 | for(var i=0; i= 0 ) {
216 | continue;
217 | }
218 | if( key == 'app' || key == 'view' || key == 'action' ) {
219 | route.action[key] = this._routes[i].params[key];
220 | }
221 | else {
222 | route.params[key] = this._routes[i].params[key];
223 | }
224 | }
225 | }
226 | else {
227 | if( !('app' in route.action) ) {
228 | route.action.app = this._routes[i].subApp;
229 | }
230 | route.path = path.substr(match[0].length);
231 | }
232 |
233 | return route;
234 | }
235 | }
236 | }
237 |
238 | return null;
239 | };
240 |
--------------------------------------------------------------------------------
/lib/cookies.js:
--------------------------------------------------------------------------------
1 | var querystring = require("querystring");
2 |
3 | // We'll be using HMAC-SHA1 to sign cookies
4 | var sha1 = require('../dependencies/sha1');
5 |
6 | /* Cookies(request, response)
7 | *
8 | * Cookies handler for Bomber, designed to work as sub-objected within both Reponse and Request.
9 | *
10 | * The is heavily inspired by the work done on cookie-node (see http://github.com/jed/cookie-node/).
11 | *
12 | * Parameters:
13 | *
14 | * + `request`: A `Request` instance.
15 | * + `response`: A `Response` instance.
16 | * + `project`: `Object`. The project.
17 | */
18 | var Cookies = exports.Cookies = function(request, response, project) {
19 | // The context for the cookie object
20 | this._request = request;
21 | this._response = response;
22 |
23 | // Secret key for signing cookies
24 | this._secret = project.config.security.signing_secret;
25 |
26 | // An object used by Cookies._prepare() to store the parsed value
27 | // of the cookie header.
28 | this._cookies = null;
29 | // An object used by Cookies.set() to store cookies to be
30 | // written in the return headers.
31 | this._output_cookies = {};
32 | };
33 |
34 | /* Cookies.prototype._prepare()
35 | *
36 | * Read the cookie header and prepare the internal
37 | * _cookies object for reading.
38 | *
39 | */
40 | Cookies.prototype._prepare = function() {
41 | // Use Node's built-in querystring module to parse the cookie
42 | this._cookies = querystring.parse(this._request.headers["cookie"] || "", sep=";", eq="=");
43 | };
44 |
45 |
46 | /* Cookies.prototype.get(name, default_value)
47 | *
48 | * Returns the value of a specific cookie, optionally falls
49 | * back on another default value or an empty string.
50 | *
51 | * When this function is called for the first time during
52 | * the request, we'll also parse the cookie header.
53 | *
54 | * Parameters:
55 | *
56 | * + `name`: `String`. The name of the cookie to be returned
57 | * + `default_value`: `Object`. A fall-back value to return if cookie doesn't exist
58 | *
59 | * Returns:
60 | *
61 | * The value of the requested cookie
62 | */
63 | Cookies.prototype.get = function(name, default_value) {
64 | if( this._cookies=== null ) {
65 | this._prepare();
66 | }
67 | if( this._cookies[name] ) {
68 | return querystring.unescape(this._cookies[name]) ;
69 | }
70 | else {
71 | return default_value || null;
72 | }
73 | };
74 |
75 | /* Cookies.prototype.keys = function(name)
76 | *
77 | * Retrieve a list of all set cookies.
78 | *
79 | * Returns:
80 | *
81 | * An `Array` of cookie variable names.
82 | */
83 | Cookies.prototype.keys = function() {
84 | if( this._cookies=== null ) {
85 | this._prepare();
86 | }
87 |
88 | var keys = [];
89 | for ( var key in this._cookies ) {
90 | keys.push(key);
91 | }
92 | return keys;
93 | }
94 |
95 | /* Cookies.prototype.set(name, value, options)
96 | *
97 | * Set a cookie to be written in the output headers.
98 | *
99 | * Parameters:
100 | *
101 | * + `name`: `String`. The name of the cookie to update.
102 | * + `value`: `String`. The new value of the cookie.
103 | * + `options`: `Object`. A key-value object specifying optional extra-options.
104 | *
105 | * Valid `options` properties are:
106 | *
107 | * + `expires`: `Date`. Cookies expiry date.
108 | * + `path`: `String`. The path where the cookie is valid.
109 | * + `domain`: `String`. Domain where the cookie is valid.
110 | * + `secure`: `Boolean`. Secure cookie?
111 | *
112 | */
113 | Cookies.prototype.set = function(name, value, options) {
114 | value = new String(value);
115 | var cookie = [ name, "=", querystring.escape(value), ";" ];
116 |
117 | options = options || {};
118 |
119 | if ( options.expires ) {
120 | cookie.push( " expires=", options.expires.toUTCString(), ";" );
121 | }
122 |
123 | if ( options.path ) {
124 | cookie.push( " path=", options.path, ";" );
125 | }
126 |
127 | if ( options.domain ) {
128 | cookie.push( " domain=", options.domain, ";" );
129 | }
130 |
131 | if ( options.secure ) {
132 | cookie.push( " secure" );
133 | }
134 |
135 | // Update local version of cookies
136 | if( this._cookies=== null ) {
137 | this._prepare();
138 | }
139 | this._cookies[name] = value;
140 |
141 | // Save the output cookie
142 | this._output_cookies[name] = cookie.join("");
143 |
144 |
145 | // Append the new cookie to headers
146 | var oc = [];
147 | for ( name in this._output_cookies ) {
148 | oc.push( this._output_cookies[name] );
149 | }
150 | this._response.headers["Set-Cookie"] = oc;
151 | };
152 |
153 | /* Cookies.prototype.unset = function(name)
154 | *
155 | * Unset a cookie by setting the value to an empty string ("")
156 | *
157 | * Parameters:
158 | *
159 | * + `name`: `String`. The name of the cookie to unset.
160 | *
161 | */
162 | Cookies.prototype.unset = function(name) {
163 | // Write an empty cookie back to client, set to expired
164 | this.set(name, '', {expires:new Date('1970-01-01')});
165 | // And remove the local version of the cookie entirely
166 | delete this._cookies[name]
167 | };
168 |
169 | /* Cookies.prototype.setSecure = function(name, value, options)
170 | *
171 | * Set a new cookie, signed by a local secret key to make sure it isn't tampered with
172 | *
173 | * The signing method deviates slightly from http://github.com/jed/cookie-node/ in that
174 | * the name of the cookie is used in the signature.
175 | *
176 | * Parameters:
177 | *
178 | * + `name`: `String`. The name of the secure cookie
179 | * + `value`: `String`. The value of the secure cookie.
180 | * + `options`: `String`. A set of options, see Cookies.set() for details
181 | *
182 | */
183 | Cookies.prototype.setSecure = function(name, value, options) {
184 | options = options || {};
185 |
186 | value = new String(value);
187 |
188 | // Note: We might want to Base-64 encode/decode the value here and in getSecure,
189 | // but I couldn't find a good library for the purpose with clear licensing.
190 | // (The SHA-1 lib does include encoding, but no decoding)
191 |
192 | var value = [ name, value, (+options.expires || "") ];
193 | var signature = sha1.hex_hmac_sha1( value.join("|"), this._secret );
194 | value.push( signature );
195 |
196 | this.set( name, value.join("|"), options );
197 | };
198 |
199 | /* Cookies.prototype.getSecure = function(name)
200 | *
201 | * Get the value for a signed cookie.
202 | *
203 | * Parameters:
204 | *
205 | * + `name`: `String`. The name of the secure cookie
206 | *
207 | * Returns:
208 | *
209 | * The value of the requested cookie if the signature is correct and the signature
210 | * hasn't expired. Otherwise `null` is returned.
211 | */
212 | Cookies.prototype.getSecure = function(name, default_value) {
213 | var raw = this.get(name);
214 | if( raw === null ) {
215 | return default_value || null;
216 | }
217 |
218 | var parts = raw.split("|");
219 |
220 | if ( parts.length !== 4 ) {
221 | return default_value || null;
222 | }
223 | if ( parts[0] !== name ) {
224 | return default_value || null;
225 | }
226 |
227 | var value = parts[1];
228 | var expires = parts[2];
229 | var cookie_signature = parts[3];
230 |
231 | if ( expires && expires < new Date() ) {
232 | // The secure cookie has expired, clear it.
233 | this.unset(name);
234 | return default_value || null;
235 | }
236 |
237 | var valid_signature = sha1.hex_hmac_sha1( parts.slice(0,3).join("|"), this._secret );
238 | if ( cookie_signature !== valid_signature) {
239 | return default_value || null;
240 | }
241 |
242 | return value;
243 | };
244 |
245 | /* Cookies.prototype.reset()
246 | *
247 | * Reset the cookies by clearing all data
248 | */
249 | Cookies.prototype.reset = function() {
250 | this.keys().forEach(function(key) {
251 | this.unset(key);
252 | }, this);
253 | };
254 |
255 | /* Cookies.prototype.exists(key)
256 | *
257 | * Returns true if a cookie by the name of `key` exists.
258 | *
259 | * Parameters:
260 | *
261 | * + `key`: `String`. The name of the cookie for which to test existence.
262 | *
263 | * Returns:
264 | *
265 | * True if the cookie exists, false if not.
266 | */
267 | Cookies.prototype.exists = function(key) {
268 | if( this._cookies === null ) {
269 | this._prepare();
270 | }
271 | return (this._cookies[key]);
272 | };
273 |
--------------------------------------------------------------------------------
/lib/session.js:
--------------------------------------------------------------------------------
1 | var sha1 = require('../dependencies/sha1');
2 |
3 | var DirectoryStore = require("./store").DirectoryStore;
4 |
5 | /* SessionManager(server)
6 | *
7 | * An in-memory object for storing session data.
8 | *
9 | * This should be initialized when the server starts up, and will
10 | * maintain the creation, renewal and expiration of sessions.
11 | *
12 | * Parameters:
13 | *
14 | * + `server`: A `Server` instance.
15 | */
16 | var SessionManager = exports.SessionManager = function(options) {
17 | this.options = options;
18 | this._sessions = {};
19 | if ( this.options.storage_method === 'disk' ) {
20 | this.store = new DirectoryStore(this.options.disk_storage_location);
21 | }
22 | };
23 |
24 |
25 | SessionManager.prototype._MAX_SESSION_KEY = Math.pow(2,63);
26 |
27 | /* SessionManager.prototype.getSession = function(request)
28 | *
29 | * Get a session for the current request.
30 | *
31 | * Tries to return `Session` object from memory, otherwise a new session is created
32 | * and stored for returning on the next request.
33 | *
34 | * Parameters:
35 | *
36 | * + `request`: A `Request` instance.
37 | *
38 | * Returns:
39 | *
40 | * Either an existing or a new Session() object depending on the context.
41 | */
42 | SessionManager.prototype.getSession = function(request) {
43 | // Get the session_key from cookies or generate a new one
44 | var session_key = request.cookies.getSecure( this.options.cookie.name );
45 | if ( !session_key ) {
46 | session_key = this._generateNewSessionKey();
47 | var new_session_cookie = true;
48 | } else {
49 | var new_session_cookie = false;
50 | }
51 |
52 | // If the session object doesn't exist in memory, we'll create one.
53 | // This will magically be loaded from persistent storage is the session_key
54 | // identifies an ongoing session.
55 | if ( !this._sessions[session_key] ) {
56 | this._sessions[session_key] = new Session(session_key, this, request);
57 | }
58 |
59 | // Write the session key to a cookie
60 | if ( new_session_cookie ) {
61 | this._sessions[session_key].renewSession();
62 | }
63 |
64 | return( this._sessions[session_key] );
65 | }
66 |
67 | /* SessionManager.prototype._generateNewSessionKey = function()
68 | *
69 | * Create a new session_key. Basically as hash of a very random string.
70 | */
71 | SessionManager.prototype._generateNewSessionKey = function() {
72 | do {
73 | // Generate a strategically random string
74 | var rnd = [Math.random()*this._MAX_SESSION_KEY, +new Date, process.pid].join('|');
75 | // And create a hash from it to use for a session key
76 | var session_key = sha1.hex_hmac_sha1( rnd, rnd );
77 | } while( this._sessions[session_key] );
78 |
79 | return( session_key );
80 | }
81 |
82 |
83 |
84 |
85 |
86 | /* Session(session_key, manager, request)
87 | *
88 | * Interact with session variables.
89 | *
90 | * Parameters:
91 | *
92 | * + `session_key`: The key for this session
93 | * + `manager`: A `SessionManager` instance.
94 | * + `request`: A `Request` instance.
95 | */
96 | var Session = exports.Session = function(session_key, manager, request) {
97 | // Remember arguments
98 | this.session_key = session_key;
99 | this._manager = manager;
100 | this._request = request;
101 | // Some state variables
102 | this._modified = false;
103 |
104 | // Try loading session data
105 | if ( this._manager.options.storage_method === 'disk' ) {
106 | // ... from disk
107 | try {
108 | // Note that we wait()ing for the data to return, since we need session immediately available
109 | this._data = this._manager.store.get('bomber-sessions', this.session_key).wait();
110 | } catch(e){}
111 | } else if ( this._manager.options.storage_method === 'cookie' ) {
112 | // ... from cookie
113 | this._data = JSON.parse(this._request.cookies.getSecure(this._manager.options.cookie.name + '__data'));
114 | }
115 |
116 | if ( this._data && (!('__expires' in this._data) || this._data['__expires']<(+new Date())) ) {
117 | // Session has expired; don't trust the data
118 | delete this._data;
119 | }
120 |
121 | if ( !('_data' in this) || !this._data ) {
122 | // If the data doesn't exist, we'll start with an empty slate
123 | this.reset();
124 | } else if ( ('__renew' in this._data) && (+new Date()) > this._data['__renew'] ) {
125 | // More than renew minutes since we last wrote the session to cookie
126 | this.renewSession();
127 | }
128 | };
129 |
130 | /* Session.prototype.set = function(name, value)
131 | *
132 | * Set a session variable
133 | *
134 | * Parameters:
135 | *
136 | * + `name`: The name of the session variable to set.
137 | * + `value`: The value of the session variable
138 | */
139 | Session.prototype.set = function(name, value) {
140 | this._data[name] = value;
141 | this.save();
142 | }
143 |
144 | /* Session.prototype.get = function(name)
145 | *
146 | * Get the value of a session variable
147 | *
148 | * Parameters:
149 | *
150 | * + `name`: The name of the session variable to retreieve.
151 | * + `default_value`: A fallback value of the variable doesn't exist.
152 | *
153 | * Returns:
154 | *
155 | * The value of the session variable
156 | */
157 | Session.prototype.get = function(name, default_value) {
158 | return( this._data[name] || default_value || null );
159 | }
160 |
161 | /* Session.prototype.unset = function(name)
162 | *
163 | * Clear or delete an existing session vairable.
164 | *
165 | * Parameters:
166 | *
167 | * + `name`: The name of the session variable to clear.
168 | */
169 | Session.prototype.unset = function(name) {
170 | delete this._data[name];
171 | this.save();
172 | }
173 |
174 | /* Session.prototype.reset = function(name)
175 | *
176 | * Reset the session by clearing all data
177 | * The session_key stays the same even after the session has been reset.
178 | *
179 | */
180 | Session.prototype.reset = function() {
181 | // Blank slate of data
182 | this._data = {__created: (+new Date)}
183 | // And renew the session cookie: New timeout and new save to storage
184 | this.renewSession();
185 | }
186 |
187 | /* Session.prototype.exists = function(name)
188 | *
189 | * Check if a certain session variable has been set or not.
190 | *
191 | * Returns:
192 | *
193 | * True if the variable has been set. False otherwise.
194 | */
195 | Session.prototype.exists = function(name) {
196 | return ( name in this._data );
197 | }
198 |
199 | /* Session.prototype.keys = function(name)
200 | *
201 | * Retrieve a list of all set sessions var.
202 | *
203 | * Returns:
204 | *
205 | * An `Array` of sesison variable names.
206 | */
207 | Session.prototype.keys = function() {
208 | var keys = [];
209 | for ( key in this._data ) {
210 | if ( !(this._data[key] instanceof Function) && key.substr(0,2) !== "__" ) {
211 | keys.push(key);
212 | }
213 | }
214 | return( keys );
215 | }
216 |
217 | /* Session.prototype.save = function()
218 | *
219 | * Notify that the session object has changes.
220 | * For cookies, we'll need to store while the request is still active. In other
221 | * cases the object will be marked as changed and save in Session.finish().
222 | */
223 | Session.prototype.save = function() {
224 | if ( this._manager.options.storage_method === 'cookie' ) {
225 | this._request.cookies.setSecure( this._manager.options.cookie.name + '__data', JSON.stringify(this._data), this._getCookieOptions() );
226 | } else {
227 | // For everything other than cookies, we'll only want one write for the entire request,
228 | // so we remember that the session was modified and continue;
229 | this._modified = true;
230 | }
231 | }
232 |
233 | /* Session.prototype.finish = function()
234 | *
235 | * Finish off the session by writing data to storage.
236 | */
237 | Session.prototype.finish = function() {
238 | if ( this._modified && this._manager.options.storage_method !== 'cookie' ) {
239 | this._manager.store.set('bomber-sessions', this.session_key, this._data);
240 | }
241 | }
242 |
243 | /* Session.prototype.renewSession = function()
244 | *
245 | * Write the session cookie to the current request connection,
246 | * including domain/path/secure from project configuration and
247 | * expires from the session timeout.
248 | */
249 | Session.prototype.renewSession = function() {
250 | // Update expiration and renew
251 | this._data['__renew'] = (+new Date( +new Date + (this._manager.options.renew_minutes*60*1000) ));
252 | this._data['__expires'] = (+new Date( +new Date + (this._manager.options.expire_minutes*60*1000) ));
253 |
254 | // Update cookie with session key
255 | this._request.cookies.setSecure( this._manager.options.cookie.name, this.session_key, this._getCookieOptions() );
256 |
257 | // And save the cleaned-up data
258 | this.save();
259 | }
260 |
261 | Session.prototype._getCookieOptions = function() {
262 | return({
263 | expires: new Date(this._data['__expires']),
264 | domain: this._manager.options.cookie.domain,
265 | path: this._manager.options.cookie.path,
266 | secure: this._manager.options.cookie.secure
267 | });
268 | }
269 |
270 |
--------------------------------------------------------------------------------
/test/test-app.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var TestSuite = require('../dependencies/node-async-testing/async_testing').TestSuite;
3 | var path = require('path');
4 |
5 | // the testing apps assume that bomberjs is on the path.
6 | require.paths.push(path.dirname(__filename)+'/../..');
7 |
8 | var App = require('bomberjs/lib/app').App;
9 | var app_errors = require('bomberjs/lib/app').errors;
10 |
11 | (new TestSuite('App Tests'))
12 | .setup(function() {
13 | this.project = {config: {}};
14 | this.project.base_app = this.app = new App('bomberjs/test/fixtures/testApp', this.project);
15 | })
16 | .runTests({
17 | "test app doesn't have to exist": function(test) {
18 | // Since all parts of an app are optional when we create an
19 | // App object if it doesn't exist we can't verify this.
20 | // It just won't do anything.
21 | test.assert.doesNotThrow(function() {
22 | var app = new App('bomberjs/test/fixtures/nonExistantApp');
23 | });
24 | },
25 | "test _inheritAndFlattenConfig": function() {
26 | var self = {
27 | 'server': { option: false },
28 | 'testApp': { one_a: 1, one_b: 1, one_c: 1, one_d: 1 },
29 | './subApp1': { option: false },
30 | './anotherApp': { option: true },
31 | '.': { one_b: 2, two_a: 2, two_b: 2, two_c: 2 },
32 | };
33 |
34 | var parent = {
35 | 'server': { option: true },
36 | 'testApp': { one_c: 3, two_b: 3, three_a: 3, three_b: 3 },
37 | './subApp1': { option: true },
38 | '.': { one_d: 4, two_c: 4, three_b: 4, four_a: 4 },
39 | };
40 |
41 | var parent_copy = process.mixin(true, {}, parent);
42 | // this properly copies right?
43 | this.assert.notEqual(parent_copy, parent);
44 |
45 | var received = this.app._inheritAndFlattenConfig(self, parent);
46 |
47 | // make sure they get combined properly
48 | var expected = {
49 | 'server': { option: true },
50 | './subApp1': { option: true },
51 | './anotherApp': { option: true },
52 | '.': { one_a: 1, one_b: 2, one_c: 3, one_d: 4, two_a: 2, two_b: 3, two_c: 4, three_a: 3, three_b: 4, four_a: 4 }
53 | };
54 | this.assert.deepEqual(expected, received);
55 |
56 | // make sure that we didn't change the parent
57 | this.assert.deepEqual(parent_copy, parent);
58 |
59 | // make sure that changing the the combined doesn't change
60 | // the parent
61 | received.server.one = 3;
62 | this.assert.deepEqual(parent_copy, parent);
63 | },
64 | "test _configForApp": function() {
65 | var start = {
66 | "server": { option: true },
67 | "one": { option: true },
68 | "two": { option: true },
69 | "./two": { option: true },
70 | "./two/three": { option: true },
71 | "./four": { option: true },
72 | };
73 |
74 | var expected_app_one = {
75 | "server": { option: true },
76 | "one": { option: true },
77 | "two": { option: true }
78 | };
79 | this.assert.deepEqual(expected_app_one, this.app._configForApp(start, 'one'));
80 |
81 | var expected_app_two = {
82 | "server": { option: true },
83 | "one": { option: true },
84 | "two": { option: true },
85 | ".": { option: true },
86 | "./three": { option: true }
87 | };
88 | this.assert.deepEqual(expected_app_two, this.app._configForApp(start, 'two'));
89 |
90 | var expected_app_three_from_app_two = {
91 | "server": { option: 1 },
92 | "one": { option: 1 },
93 | "two": { option: 1 },
94 | ".": { option: 1 }
95 | };
96 | this.assert.deepEqual(expected_app_three_from_app_two, this.app._configForApp(expected_app_two, 'three'));
97 |
98 | var expected_app_four = {
99 | "server": { option: 1 },
100 | "one": { option: 1 },
101 | "two": { option: 1 },
102 | ".": { option: 1 }
103 | };
104 | this.assert.deepEqual(expected_app_four, this.app._configForApp(start, 'four'));
105 | },
106 | "test load config": function(test) {
107 | test.assert.equal(1, test.app.config.option_one);
108 | test.assert.equal(2, test.app.config.option_two);
109 | },
110 | "test load subapps": function(test) {
111 | //base test app has 1 sub app
112 | test.assert.equal(1, count(test.app.apps));
113 |
114 | // first sub app has config passed to it from testApp
115 | test.assert.deepEqual({option: true}, test.app.apps.subApp1.config);
116 | },
117 | "test subapp's parent is set": function(test) {
118 | test.assert.equal(test.app, test.app.apps.subApp1.parent);
119 | },
120 | "test can load non-existant view": function(test) {
121 | // can't get a view that doesn't exist
122 | test.assert.throws(function() {
123 | test.app.getView('non-existant')
124 | }, app_errors.ViewNotFoundError );
125 |
126 | // can't get a view from an app that doesn't exist
127 | test.assert.throws(function() {
128 | test.app.getView('view1', 'not-existant-app')
129 | }, app_errors.AppNotFoundError );
130 | },
131 | "test load view": function(test) {
132 | test.assert.ok(test.app.getView('view1'));
133 | },
134 | "test load view from sub-app": function(test) {
135 | // can dig down to get a view file from a subapp
136 | var subAppView = test.app.getView('subApp1view1','subApp1');
137 | test.assert.ok(subAppView);
138 | // this view has 1 function
139 | test.assert.equal(1, count(subAppView));
140 | },
141 | "test load routes": function(test) {
142 | // test that we properly load in the routes
143 | test.assert.equal(3, test.app.router._routes.length);
144 | test.assert.equal(2, test.app.apps.subApp1.router._routes.length);
145 | },
146 | "test getRoute will pass routing along": function(test) {
147 | // getRoute will pass the routing along if an app_key is passed in that
148 | // points to a sub app
149 | var route = test.app.getRoute('GET', '/view_name/action_name', 'subApp1');
150 | var expected = {
151 | "action": {
152 | "app": "subApp1",
153 | "view": "view_name",
154 | "action": "action_name"
155 | },
156 | "params": {}
157 | };
158 | test.assert.deepEqual(expected, route);
159 | },
160 | "test getRoute will pass routing along if it gets a partial route": function(test) {
161 | // getRoute will pass the routing along if it gets a partial route back
162 | // from the router
163 | var route = test.app.getRoute('GET', '/deferToSubApp1/view_name/action_name');
164 | var expected = {
165 | "action": {
166 | "app": "subApp1",
167 | "view": "view_name",
168 | "action": "action_name"
169 | },
170 | "params": {}
171 | };
172 | test.assert.deepEqual(expected, route);
173 | route = test.app.getRoute('GET', '/deferToSubApp1/view_name/action_name/1');
174 | expected = {
175 | "action": {
176 | "app": "subApp1",
177 | "view": "view_name",
178 | "action": "action_name"
179 | },
180 | "params": {id: "1"}
181 | };
182 | test.assert.deepEqual(expected, route);
183 | },
184 | "test errors are thrown appropriately": function(test) {
185 | test.assert.throws(function() {
186 | test.app.getAction({ app: 'non-existant', view: 'subApp1view1', action: 'action'});
187 | }, app_errors.AppNotFoundError);
188 | test.assert.throws(function() {
189 | test.app.getAction({ app: 'subApp1', view: 'non-existant', action: 'action'});
190 | }, app_errors.ViewNotFoundError);
191 | test.assert.throws(function() {
192 | test.app.getAction({ app: 'subApp1', view: 'subApp1view1', action: 'non-existant'});
193 | }, app_errors.ActionNotFoundError);
194 | },
195 | "test can load action": function(test) {
196 | test.assert.ok(test.app.getAction({ app: 'subApp1', view: 'subApp1view1', action: 'action'}));
197 | },
198 | "test can specify a function in a route": function(test) {
199 | var func = function() {};
200 | test.assert.equal(func, test.app.getAction({action: func}));
201 | },
202 | "test _parseAppPath": function(test) {
203 | var app_keys = {
204 | '': [],
205 | '.': [],
206 | './': [],
207 | '/': [],
208 | 'testApp': [],
209 | '/testApp': [],
210 | './subApp': ['subApp'],
211 | '/subApp': ['subApp'],
212 | 'subApp': ['subApp'],
213 | 'testApp/subApp': ['subApp'],
214 | '/testApp/subApp': ['subApp'],
215 | './sub/app/stuff': ['sub', 'app/stuff'],
216 | '/sub/app/stuff': ['sub', 'app/stuff'],
217 | 'sub/app/stuff': ['sub', 'app/stuff'],
218 | 'testApp/sub/app/stuff': ['sub', 'app/stuff'],
219 | '/testApp/sub/app/stuff': ['sub', 'app/stuff']
220 | };
221 | for(var key in app_keys) {
222 | test.assert.deepEqual(app_keys[key], test.app._parseAppPath(key));
223 | }
224 | },
225 | "test modulePathToKey": function(test) {
226 | test.assert.equal(path.basename(process.cwd()), App.modulePathToAppKey('.'));
227 | test.assert.equal('path', App.modulePathToAppKey('./my/path'));
228 | test.assert.equal('path', App.modulePathToAppKey('/my/path'));
229 | test.assert.equal('path', App.modulePathToAppKey('my/path'));
230 | test.assert.equal('path', App.modulePathToAppKey('./path'));
231 | test.assert.equal('path', App.modulePathToAppKey('/path'));
232 | test.assert.equal('path', App.modulePathToAppKey('path'));
233 | }
234 | });
235 |
236 | function count(object) {
237 | var count = 0;
238 | for( var key in object ) {
239 | count++;
240 | }
241 | return count;
242 | }
243 |
244 |
--------------------------------------------------------------------------------
/lib/store.js:
--------------------------------------------------------------------------------
1 | // Simple file-based storage of JSON objects
2 | // Guan Yang (guan@yang.dk), 2010-01-30
3 |
4 | // Very simple persistent storage for JSON objects. I rely heavily on
5 | // POSIX filesystem semantics. This store is not very efficient because
6 | // it stores each document as a separate file, to facilitate atomic
7 | // writes in an easy way, but it should scale reasonably well on modern
8 | // filesystems.
9 |
10 | // No locks are necessary or possible. There is no multi-version
11 | // concurrency control (MVCC), but this may be added later.
12 |
13 | var posix = require("posix"),
14 | path = require("path"),
15 | sys = require("sys"),
16 | events = require("events"),
17 | sha1 = require('../dependencies/sha1');
18 |
19 | /* DirectoryStore()
20 | *
21 | * DirectoryStore provides a very simple persistent storage system for
22 | * JSON objects. It relies heavily on POSIX filesystem semantics to
23 | * enable atomic writes. It is not very efficient because it stores
24 | * each document in a separate file, but it should scale reasonably well
25 | * on modern filesystems that support a large number of files in a
26 | * directory.
27 | */
28 | var DirectoryStore = exports.DirectoryStore = function (location) {
29 | var readmePath = path.join(location, "DirectoryStore.txt");
30 |
31 | validNamespaces = {};
32 |
33 | var createLocation = function () {
34 | posix.mkdir(location, 0700).wait();
35 | };
36 |
37 | var createReadmeFile = function () {
38 | var fd =
39 | posix.open(readmePath, process.O_CREAT | process.O_WRONLY, 0644)
40 | .wait();
41 | posix.write(fd,
42 | "This is a data storage location for DirectoryStore.\r\n").wait();
43 | posix.close(fd).wait();
44 | };
45 |
46 | /* getLocation()
47 | *
48 | * Returns the directory path used by the data store.
49 | */
50 | var getLocation = function (location) {
51 | return this.location;
52 | };
53 | this.getLocation = getLocation;
54 |
55 | /* assertNamespace(namespace)
56 | *
57 | * Ensure that a namespace exists, and if it does not, create it.
58 | */
59 | var assertNamespace = function (namespace) {
60 | // Our cache of already created namespaces
61 | if (validNamespaces.hasOwnProperty(namespace)) {
62 | return true;
63 | }
64 |
65 | var namespacePath = path.join(location, sha1.hex_sha1(namespace));
66 | var nameFile = namespacePath + ".namespace";
67 | var p;
68 |
69 | // TODO: rewrite to use promises
70 | try {
71 | try {
72 | nameStats = posix.stat(nameFile).wait();
73 | } catch (e) {
74 | if (e.message !== "No such file or directory") {
75 | throw e;
76 | } else {
77 | p = atomicWrite(nameFile, JSON.stringify(namespace), 0600);
78 | p.wait();
79 | }
80 | }
81 |
82 | stats = posix.stat(namespacePath).wait();
83 |
84 | if (!stats.isDirectory()) {
85 | // Oh boy
86 | throw {
87 | message: "Namespace exists but is not a directory"
88 | }
89 | }
90 | } catch (e) {
91 | if (e.message !== "No such file or directory") {
92 | throw e;
93 | }
94 |
95 | posix.mkdir(namespacePath, 0700).wait();
96 | }
97 |
98 | // We're okay
99 | validNamespaces[namespace] = namespacePath;
100 | return true;
101 | };
102 |
103 | var valuePath = function(namespace, key) {
104 | // Values are stored in a file with the sha1
105 | var namespacePart = sha1.hex_sha1(namespace);
106 |
107 | // If key is not a string, JSON it
108 | if (typeof key !== "string") {
109 | key = JSON.stringify(key);
110 | }
111 |
112 | var keyPart = sha1.hex_sha1(key);
113 |
114 | if (key.length) {
115 | keyPart += "-" + key.length;
116 | }
117 |
118 | return path.join(location, namespacePart, keyPart);
119 | }
120 |
121 | /* get(namespace, key)
122 | *
123 | * Retrieve a value from the directory store based on namespace and key.
124 | * Both namespace and key SHOULD be strings.
125 | */
126 | var get = function(namespace, key) {
127 | var filename = valuePath(namespace, key);
128 | var p = new events.Promise();
129 | var catPromise = posix.cat(filename);
130 |
131 | catPromise.addCallback(function(content) {
132 | p.emitSuccess(JSON.parse(content));
133 | });
134 |
135 | catPromise.addErrback(function(e) {
136 | p.emitError(e);
137 | });
138 |
139 | return p;
140 | };
141 | this.get = get;
142 |
143 | /* unset(namespace, key)
144 | *
145 | * Remove a value from the directory store.
146 | */
147 | var unset = function(namespace, key) {
148 | var filename = valuePath(namespace, key);
149 | var key_filename = filename + ".key";
150 | posix.unlink(key_filename);
151 | return posix.unlink(filename);
152 | }
153 | this.unset = unset;
154 |
155 | var atomicWrite = function(filename, value, mode) {
156 | if (typeof value !== "string") {
157 | throw {
158 | message: "Value must be a string"
159 | }
160 | }
161 |
162 | var p = new events.Promise();
163 |
164 | /* Create a proper temp filename */
165 | var date = new Date();
166 | var tmp = process.platform + "." + process.pid + "." +
167 | Math.round(Math.random()*1e6) + "." + (+date) + "." +
168 | date.getMilliseconds();
169 | var filenameTmp = filename + "." + tmp + ".tmp";
170 |
171 | var openPromise = posix.open(filenameTmp,
172 | process.O_CREAT|process.O_WRONLY, mode);
173 |
174 | openPromise.addCallback(function(fd) {
175 | var writePromise = posix.write(fd, value);
176 | writePromise.addCallback(function(written) {
177 | var closePromise = posix.close(fd);
178 | closePromise.addCallback(function() {
179 | var renamePromise = posix.rename(filenameTmp, filename);
180 | renamePromise.addCallback(function() {
181 | // Rename was successful
182 | p.emitSuccess();
183 | });
184 | renamePromise.addErrback(function(e) {
185 | // Rename failed, remove the tmp but don't
186 | // wait for that.
187 | posix.unlink(filenameTmp);
188 | p.emitError(e);
189 | });
190 | });
191 | closePromise.addErrback(function(e) {
192 | // Close failed, remove the tmp and complain, no wait
193 | posix.unlink(filenameTmp);
194 | p.emitError(e);
195 | });
196 | });
197 | writePromise.addErrback(function(e) {
198 | // write failed, close and remove the tmp, again, don't
199 | // wait
200 | var writeFailPromise = posix.close(fd);
201 | writeFailPromise.addCallback(function() {
202 | posix.unlink(filenameTmp);
203 | });
204 | writeFailPromise.addErrback(function(e) {
205 | posix.unlink(filenameTmp);
206 | });
207 |
208 | p.emitError(e);
209 | });
210 | });
211 |
212 | openPromise.addErrback(function(e) {
213 | // open failed, complain
214 | p.emitError(e);
215 | });
216 |
217 | return p;
218 | }
219 |
220 | /* set(namespace, key, value)
221 | *
222 | * Save a value to the directory store indexed by namespace and key.
223 | * Both namespace and key SHOULD be strings. value can be any value that
224 | * can be serialized by JSON.stringify.
225 | */
226 | var set = function(namespace, key, value) {
227 | assertNamespace(namespace);
228 | var filename = valuePath(namespace, key);
229 | var key_filename = filename + ".key";
230 |
231 | var p = new events.Promise();
232 |
233 | var keyPromise = atomicWrite(key_filename, JSON.stringify(key), 0600);
234 | keyPromise.addCallback(function() {
235 | var writePromise = atomicWrite(filename, JSON.stringify(value), 0600);
236 | writePromise.addCallback(function() {
237 | p.emitSuccess();
238 | });
239 | writePromise.addErrback(function(e) {
240 | p.emitError(e);
241 | });
242 | });
243 | keyPromise.addErrback(function(e) {
244 | p.emitError(e);
245 | });
246 |
247 | return p;
248 | };
249 | this.set = set;
250 |
251 |
252 |
253 | /* close()
254 | *
255 | * This is a NOOP in DirectoryStore, but may be useful for other types of
256 | * data stores.
257 | */
258 | var close = function() {
259 | /* we need to do something; we'll stat the readme file. */
260 | return posix.stat(readmePath);
261 | };
262 | this.close = close;
263 |
264 | // If the file exists, make sure that it is a directory and that it
265 | // writable.
266 | try {
267 | stats = posix.stat(location).wait();
268 |
269 | if (!stats.isDirectory()) {
270 | throw {
271 | message: "Path exists but is not a directory"
272 | };
273 | }
274 |
275 | // We unlink every time to make sure that we have the correct
276 | // permissions.
277 | try {
278 | posix.unlink(readmePath).wait();
279 | createReadmeFile();
280 | } catch (e) {
281 | if (e.message !== "No such file or directory") {
282 | throw e;
283 | } else {
284 | createReadmeFile();
285 | }
286 | }
287 | } catch (e) {
288 | if (e.message !== "No such file or directory") {
289 | throw e;
290 | } else {
291 | createLocation();
292 | createReadmeFile();
293 | }
294 | }
295 | };
296 |
--------------------------------------------------------------------------------
/dependencies/sha1.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
3 | * in FIPS 180-1
4 | * Version 2.2 Copyright Paul Johnston 2000 - 2009.
5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
6 | * Distributed under the BSD License
7 | * See http://pajhome.org.uk/crypt/md5 for details.
8 | *
9 | * Modified for Node.JS only in lines 35-41 to export functions upon require()
10 | *
11 | */
12 |
13 | /*
14 | * Configurable variables. You may need to tweak these to be compatible with
15 | * the server-side, but the defaults work in most cases.
16 | */
17 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
18 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
19 |
20 | /*
21 | * These are the functions you'll usually want to call
22 | * They take string arguments and return either hex or base-64 encoded strings
23 | */
24 | function hex_sha1(s) { return rstr2hex(rstr_sha1(str2rstr_utf8(s))); }
25 | function b64_sha1(s) { return rstr2b64(rstr_sha1(str2rstr_utf8(s))); }
26 | function any_sha1(s, e) { return rstr2any(rstr_sha1(str2rstr_utf8(s)), e); }
27 | function hex_hmac_sha1(k, d)
28 | { return rstr2hex(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); }
29 | function b64_hmac_sha1(k, d)
30 | { return rstr2b64(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); }
31 | function any_hmac_sha1(k, d, e)
32 | { return rstr2any(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
33 |
34 |
35 | // Expose functions to the module (* the only change made to original code *)
36 | exports.hex_sha1=hex_sha1;
37 | exports.b64_sha1=b64_sha1;
38 | exports.any_sha1=any_sha1;
39 | exports.hex_hmac_sha1=hex_hmac_sha1;
40 | exports.b64_hmac_sha1=b64_hmac_sha1;
41 | exports.any_hmac_sha1=any_hmac_sha1;
42 |
43 | /*
44 | * Perform a simple self-test to see if the VM is working
45 | */
46 | function sha1_vm_test()
47 | {
48 | return hex_sha1("abc").toLowerCase() == "a9993e364706816aba3e25717850c26c9cd0d89d";
49 | }
50 |
51 | /*
52 | * Calculate the SHA1 of a raw string
53 | */
54 | function rstr_sha1(s)
55 | {
56 | return binb2rstr(binb_sha1(rstr2binb(s), s.length * 8));
57 | }
58 |
59 | /*
60 | * Calculate the HMAC-SHA1 of a key and some data (raw strings)
61 | */
62 | function rstr_hmac_sha1(key, data)
63 | {
64 | var bkey = rstr2binb(key);
65 | if(bkey.length > 16) bkey = binb_sha1(bkey, key.length * 8);
66 |
67 | var ipad = Array(16), opad = Array(16);
68 | for(var i = 0; i < 16; i++)
69 | {
70 | ipad[i] = bkey[i] ^ 0x36363636;
71 | opad[i] = bkey[i] ^ 0x5C5C5C5C;
72 | }
73 |
74 | var hash = binb_sha1(ipad.concat(rstr2binb(data)), 512 + data.length * 8);
75 | return binb2rstr(binb_sha1(opad.concat(hash), 512 + 160));
76 | }
77 |
78 | /*
79 | * Convert a raw string to a hex string
80 | */
81 | function rstr2hex(input)
82 | {
83 | try { hexcase } catch(e) { hexcase=0; }
84 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
85 | var output = "";
86 | var x;
87 | for(var i = 0; i < input.length; i++)
88 | {
89 | x = input.charCodeAt(i);
90 | output += hex_tab.charAt((x >>> 4) & 0x0F)
91 | + hex_tab.charAt( x & 0x0F);
92 | }
93 | return output;
94 | }
95 |
96 | /*
97 | * Convert a raw string to a base-64 string
98 | */
99 | function rstr2b64(input)
100 | {
101 | try { b64pad } catch(e) { b64pad=''; }
102 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
103 | var output = "";
104 | var len = input.length;
105 | for(var i = 0; i < len; i += 3)
106 | {
107 | var triplet = (input.charCodeAt(i) << 16)
108 | | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
109 | | (i + 2 < len ? input.charCodeAt(i+2) : 0);
110 | for(var j = 0; j < 4; j++)
111 | {
112 | if(i * 8 + j * 6 > input.length * 8) output += b64pad;
113 | else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
114 | }
115 | }
116 | return output;
117 | }
118 |
119 | /*
120 | * Convert a raw string to an arbitrary string encoding
121 | */
122 | function rstr2any(input, encoding)
123 | {
124 | var divisor = encoding.length;
125 | var remainders = Array();
126 | var i, q, x, quotient;
127 |
128 | /* Convert to an array of 16-bit big-endian values, forming the dividend */
129 | var dividend = Array(Math.ceil(input.length / 2));
130 | for(i = 0; i < dividend.length; i++)
131 | {
132 | dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
133 | }
134 |
135 | /*
136 | * Repeatedly perform a long division. The binary array forms the dividend,
137 | * the length of the encoding is the divisor. Once computed, the quotient
138 | * forms the dividend for the next step. We stop when the dividend is zero.
139 | * All remainders are stored for later use.
140 | */
141 | while(dividend.length > 0)
142 | {
143 | quotient = Array();
144 | x = 0;
145 | for(i = 0; i < dividend.length; i++)
146 | {
147 | x = (x << 16) + dividend[i];
148 | q = Math.floor(x / divisor);
149 | x -= q * divisor;
150 | if(quotient.length > 0 || q > 0)
151 | quotient[quotient.length] = q;
152 | }
153 | remainders[remainders.length] = x;
154 | dividend = quotient;
155 | }
156 |
157 | /* Convert the remainders to the output string */
158 | var output = "";
159 | for(i = remainders.length - 1; i >= 0; i--)
160 | output += encoding.charAt(remainders[i]);
161 |
162 | /* Append leading zero equivalents */
163 | var full_length = Math.ceil(input.length * 8 /
164 | (Math.log(encoding.length) / Math.log(2)))
165 | for(i = output.length; i < full_length; i++)
166 | output = encoding[0] + output;
167 |
168 | return output;
169 | }
170 |
171 | /*
172 | * Encode a string as utf-8.
173 | * For efficiency, this assumes the input is valid utf-16.
174 | */
175 | function str2rstr_utf8(input)
176 | {
177 | var output = "";
178 | var i = -1;
179 | var x, y;
180 |
181 | while(++i < input.length)
182 | {
183 | /* Decode utf-16 surrogate pairs */
184 | x = input.charCodeAt(i);
185 | y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
186 | if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
187 | {
188 | x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
189 | i++;
190 | }
191 |
192 | /* Encode output as utf-8 */
193 | if(x <= 0x7F)
194 | output += String.fromCharCode(x);
195 | else if(x <= 0x7FF)
196 | output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
197 | 0x80 | ( x & 0x3F));
198 | else if(x <= 0xFFFF)
199 | output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
200 | 0x80 | ((x >>> 6 ) & 0x3F),
201 | 0x80 | ( x & 0x3F));
202 | else if(x <= 0x1FFFFF)
203 | output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
204 | 0x80 | ((x >>> 12) & 0x3F),
205 | 0x80 | ((x >>> 6 ) & 0x3F),
206 | 0x80 | ( x & 0x3F));
207 | }
208 | return output;
209 | }
210 |
211 | /*
212 | * Encode a string as utf-16
213 | */
214 | function str2rstr_utf16le(input)
215 | {
216 | var output = "";
217 | for(var i = 0; i < input.length; i++)
218 | output += String.fromCharCode( input.charCodeAt(i) & 0xFF,
219 | (input.charCodeAt(i) >>> 8) & 0xFF);
220 | return output;
221 | }
222 |
223 | function str2rstr_utf16be(input)
224 | {
225 | var output = "";
226 | for(var i = 0; i < input.length; i++)
227 | output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
228 | input.charCodeAt(i) & 0xFF);
229 | return output;
230 | }
231 |
232 | /*
233 | * Convert a raw string to an array of big-endian words
234 | * Characters >255 have their high-byte silently ignored.
235 | */
236 | function rstr2binb(input)
237 | {
238 | var output = Array(input.length >> 2);
239 | for(var i = 0; i < output.length; i++)
240 | output[i] = 0;
241 | for(var i = 0; i < input.length * 8; i += 8)
242 | output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (24 - i % 32);
243 | return output;
244 | }
245 |
246 | /*
247 | * Convert an array of big-endian words to a string
248 | */
249 | function binb2rstr(input)
250 | {
251 | var output = "";
252 | for(var i = 0; i < input.length * 32; i += 8)
253 | output += String.fromCharCode((input[i>>5] >>> (24 - i % 32)) & 0xFF);
254 | return output;
255 | }
256 |
257 | /*
258 | * Calculate the SHA-1 of an array of big-endian words, and a bit length
259 | */
260 | function binb_sha1(x, len)
261 | {
262 | /* append padding */
263 | x[len >> 5] |= 0x80 << (24 - len % 32);
264 | x[((len + 64 >> 9) << 4) + 15] = len;
265 |
266 | var w = Array(80);
267 | var a = 1732584193;
268 | var b = -271733879;
269 | var c = -1732584194;
270 | var d = 271733878;
271 | var e = -1009589776;
272 |
273 | for(var i = 0; i < x.length; i += 16)
274 | {
275 | var olda = a;
276 | var oldb = b;
277 | var oldc = c;
278 | var oldd = d;
279 | var olde = e;
280 |
281 | for(var j = 0; j < 80; j++)
282 | {
283 | if(j < 16) w[j] = x[i + j];
284 | else w[j] = bit_rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
285 | var t = safe_add(safe_add(bit_rol(a, 5), sha1_ft(j, b, c, d)),
286 | safe_add(safe_add(e, w[j]), sha1_kt(j)));
287 | e = d;
288 | d = c;
289 | c = bit_rol(b, 30);
290 | b = a;
291 | a = t;
292 | }
293 |
294 | a = safe_add(a, olda);
295 | b = safe_add(b, oldb);
296 | c = safe_add(c, oldc);
297 | d = safe_add(d, oldd);
298 | e = safe_add(e, olde);
299 | }
300 | return Array(a, b, c, d, e);
301 |
302 | }
303 |
304 | /*
305 | * Perform the appropriate triplet combination function for the current
306 | * iteration
307 | */
308 | function sha1_ft(t, b, c, d)
309 | {
310 | if(t < 20) return (b & c) | ((~b) & d);
311 | if(t < 40) return b ^ c ^ d;
312 | if(t < 60) return (b & c) | (b & d) | (c & d);
313 | return b ^ c ^ d;
314 | }
315 |
316 | /*
317 | * Determine the appropriate additive constant for the current iteration
318 | */
319 | function sha1_kt(t)
320 | {
321 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 :
322 | (t < 60) ? -1894007588 : -899497514;
323 | }
324 |
325 | /*
326 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally
327 | * to work around bugs in some JS interpreters.
328 | */
329 | function safe_add(x, y)
330 | {
331 | var lsw = (x & 0xFFFF) + (y & 0xFFFF);
332 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
333 | return (msw << 16) | (lsw & 0xFFFF);
334 | }
335 |
336 | /*
337 | * Bitwise rotate a 32-bit number to the left.
338 | */
339 | function bit_rol(num, cnt)
340 | {
341 | return (num << cnt) | (num >>> (32 - cnt));
342 | }
343 |
--------------------------------------------------------------------------------
/lib/response.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys'),
2 | posix = require('posix');
3 |
4 | var responses = require('./http_responses'),
5 | utils = require('./utils');
6 |
7 | /* Response(response)
8 | *
9 | * The Response object is a wrapper around the Node's `http.ServerResponse`
10 | * object.
11 | *
12 | * It makes manipulating a response easier.
13 | *
14 | * Now, instead of having to worry about calling `sendHeader` or `finish`, you
15 | * just set properties on the response (like `status`, `mimeType`,
16 | * `charset`, or `headers['X']`) and then when you call `send` it will do all
17 | * that for you.
18 | *
19 | * However, if you want the fine grained control, you can still do things the
20 | * old way (by calling `sendHeaders` or setting `response.finishOnSend` to false
21 | * yourself).
22 | *
23 | * Eventually we'll make it do other things as well, like making it easier to
24 | * set cookies and session variables.
25 | *
26 | * Parameters:
27 | *
28 | * + `response`: a `http.ServerResponse` instance.
29 | */
30 | var Response = exports.Response = function(response) {
31 | this._response = response;
32 | this.headers = {};
33 | };
34 |
35 |
36 | //TODO Make these defaults loaded in from config.js in the app.
37 |
38 | /* Response.__defaultMimeType
39 | *
40 | * If `response.mimeType` isn't set, this value is used
41 | */
42 | Response.__defaultMimeType = 'text/html';
43 | /* Response.__defaultTransferEncoding
44 | *
45 | * The default encoding used for `http.serverResponse.sendBody(chunk, encoding)`
46 | */
47 | Response.__defaultTransferEncoding = 'utf8';
48 | /* Response.__bytesToRead
49 | *
50 | * In `Response.prototype.sendFile()` we only read the file in this many bytes at
51 | * a time. This way if it is a large file, we won't try to load the whole thing
52 | * into memory at once.
53 | */
54 | Response.__bytesToRead = 16384; // 16 * 1024
55 |
56 | /* Response.prototype.build
57 | *
58 | * Shortcut to require('bomberjs/lib/http_responses') for making easy/quick
59 | * responses.
60 | */
61 | Response.prototype.build = responses;
62 |
63 | /* Response.prototype.finishOnSend
64 | *
65 | * If when `Response.prototype.send()` is called, should
66 | * `Response.prototype.finish()` also be called?
67 | */
68 | Response.prototype.finishOnSend = true;
69 |
70 | /* Response.prototype.transferEncoding
71 | *
72 | * The encoding used for `http.serverResponse.sendBody(chunk, encoding)`
73 | */
74 | Response.prototype.transferEncoding = Response.__defaultTransferEncoding;
75 |
76 | /* Response.prototype.status
77 | *
78 | * The HTTP status for this response
79 | */
80 | Response.prototype.status = 200;
81 |
82 | // we set these to null so we can know if they were explicitly set.
83 | // if they weren't we'll try and fill them in ourselves
84 |
85 | /* Response.prototype.mimeType
86 | *
87 | * Used to fill in the Content-Type header if it isn't set. If this is
88 | * `null`, `Response.__defaultMimeType` is used.
89 | */
90 | Response.prototype.mimeType = null;
91 | /* Response.prototype.charset
92 | *
93 | * Used to fill in the Content-Type header if it isn't set. If this is
94 | * `null`, `utils.charsets.lookup()` is called with
95 | * `Response.prototype.mimeType` and that return value is used.
96 | */
97 | Response.prototype.charset = null;
98 |
99 | /* variables for keeping track of internal state */
100 |
101 | /* Response.prototype._sentHeaders
102 | *
103 | * Used internally to know if the headers have already been sent.
104 | */
105 | Response.prototype._sentHeaders = false;
106 | /* Response.prototype._finished
107 | *
108 | * Used internally to know if the response has been finished.
109 | */
110 | Response.prototype._finished = false;
111 |
112 | /* Response.prototype.finish()
113 | *
114 | * Finish this request, closing the connection with the client.
115 | * A wrapper around `http.ServerResponse.finish`
116 | *
117 | * Sends the headers if they haven't already been sent.
118 | *
119 | * Throws:
120 | *
121 | * Throws an error if the response has already been finished.
122 | */
123 | Response.prototype.finish = function() {
124 | if( !this._sentHeaders ) {
125 | this.sendHeaders();
126 | }
127 |
128 | if( this._finished ) {
129 | //TODO throw custom error here
130 | throw "Response has already finished";
131 | }
132 |
133 | this._finished = true;
134 | this._response.finish();
135 | };
136 |
137 | /* Response.prototype.setHeader(key, value)
138 | *
139 | * A way to set the header of an object. Will throw an error if the
140 | * headers have already been sent.
141 | *
142 | * You can also just access the headers object directly. The
143 | * following are practicially equivalent:
144 | *
145 | * response.setHeader('Content-Type', 'text');
146 | * response.headers['Content-Type'] = 'text';
147 | *
148 | * The only difference is that the former checks that the headers
149 | * haven't already been sent, and throws an error if they have.
150 | *
151 | * Parameters:
152 | *
153 | * + `key`: `String`. The name of the header to set.
154 | * + `value`: `String`. The value for the header.
155 | *
156 | * Throws:
157 | *
158 | * Throws an error if the headers have already been sent.
159 | */
160 | Response.prototype.setHeader = function(key, value) {
161 | if( this._sentHeaders ) {
162 | //TODO throw custom error here
163 | throw "Headers already sent";
164 | }
165 | this.headers[key] = value;
166 | };
167 |
168 | /* Response.prototype.sendHeaders()
169 | *
170 | * Wrapper around http.ServerRespose.sendHeader
171 | *
172 | * Throws an error if the headers have already been sent. Also sets the
173 | * Content-Type header if it isn't set. It uses
174 | * `Response.prototype.mimeType` and `Response.prototype.charset` to set the
175 | * Content-Type header.
176 | */
177 | Response.prototype.sendHeaders = function() {
178 | if( this._sentHeaders ) {
179 | throw "Headers have already been sent!";
180 | }
181 |
182 | this._sentHeaders = true;
183 |
184 | // Check to see if the Content-Type was explicitly set. If not use
185 | // this.mimeType and this.charset (if applicable). And if that isn't set
186 | // use the default.
187 | if( !('Content-Type' in this.headers) ) {
188 | this.mimeType = this.mimeType || Response.__defaultMimeType;
189 | this.charset = this.charset || utils.charsets.lookup(this.mimeType);
190 | this.headers['Content-Type'] = this.mimeType + (this.charset ? '; charset=' + this.charset : '');
191 | }
192 |
193 | // this is a complete hack that we'll be able to remove soon. It makes it so
194 | // we can send headers multiple times. Node doesn't have an easy way to
195 | // do this currently, but people are working on it.
196 | // How this works is it checks to see if the value for a header is an Array
197 | // and if it is, use the hack from this thread:
198 | // http://groups.google.com/group/nodejs/browse_thread/thread/76ccd1714bbf54f6/56c1696da9a52061?lnk=gst&q=multiple+headers#56c1696da9a52061
199 | // to get it to work.
200 | for( var key in this.headers ) {
201 | if( this.headers[key].constructor == Array ) {
202 | var count = 1;
203 | this.headers[key].forEach(function(header) {
204 | while(this.headers[key+count]) {
205 | count++;
206 | }
207 | this.headers[key+count] = [key, header];
208 | count++;
209 | },this);
210 | delete this.headers[key];
211 | }
212 | }
213 |
214 | this._response.sendHeader(this.status, this.headers);
215 | };
216 |
217 | /* Response.prototype.send(str)
218 | *
219 | * Very similar to `http.ServerResponse.sendBody` except it handles sending the
220 | * headers if they haven't already been sent.
221 | *
222 | * Will also call `Response.prototype.finish()` if
223 | * `Response.prototype.finishOnSend` is set.
224 | *
225 | * Parameters:
226 | *
227 | * + `str`: `String`. The string to be sent to the client.
228 | */
229 | Response.prototype.send = function(str) {
230 | if( this._finished ) {
231 | //TODO throw custom error here
232 | throw "Response has already finished";
233 | }
234 |
235 | /* TODO: check to see if this.finishOnSend is set, and if so
236 | * set the content-length header */
237 |
238 | if( !this._sentHeaders ) {
239 | this.sendHeaders();
240 | }
241 |
242 | this._response.sendBody(str, this.transferEncoding);
243 |
244 | if(this.finishOnSend) {
245 | this.finish();
246 | }
247 | };
248 |
249 | /* Response.prototype.sendFile(filename)
250 | *
251 | * Acts like `Response.prototype.send()` except it sends the contents of the
252 | * file specified by `filename`.
253 | *
254 | * Makes sending files really easy. Give it a filename, and it
255 | * will read the file in chunks (if it is a big file) and
256 | * send them each back to the client.
257 | *
258 | * Uses `Response.__bytesToRead` to read the file in.
259 | *
260 | * Parameters:
261 | *
262 | * + `filename`: `String`. The name of the file to load and send as part of the request.
263 | *
264 | * Returns:
265 | *
266 | * A Promise which will emitSuccess if everything goes well, or emitError if
267 | * there is a problem.
268 | *
269 | * Throws:
270 | *
271 | * Potentially throws the same things as `Response.prototype.send()`. The
272 | * returned Promise will also emit the same errors as `posix.open` and
273 | * `posix.read`.
274 | */
275 | Response.prototype.sendFile = function(filename) {
276 | // to be able to notify scripts when this is done sending
277 | var p = new process.Promise();
278 |
279 | // if we haven't sent the headers and a mimeType hasn't been specified
280 | // for this response, then look one up for this file
281 | if( !this._sentHeaders && this.mimeType === null ) {
282 | this.mimeType = utils.mime.lookup(filename);
283 | }
284 |
285 | // We want to use the binary encoding for reading from the file and sending
286 | // to the server so the file is transferred exactly. So we save the
287 | // current encoding to set it back later.
288 | var previousEncoding = this.transferEncoding;
289 | this.transferEncoding = 'binary';
290 |
291 | // We're going to send the response in chunks, so we are going to change
292 | // finishOnSend. We want to set it back to what it was before when we are done.
293 | var previousFinishOnSend = this.finishOnSend;
294 | this.finishOnSend = false;
295 |
296 | // bind our callbacks functions to this response
297 | var self = this;
298 |
299 | var errback = function(e) {
300 | // set this back to what it was before since we are about to break out of
301 | // what we are doing...
302 | self.finishOnSend = previousFinishOnSend;
303 | self.transferEncoding = previousEncoding;
304 | p.emitError(e);
305 | };
306 |
307 | posix.open(filename, process.O_RDONLY , 0666).addCallback(function(fd) {
308 | var pos = 0;
309 |
310 | var callback = function(chunk, bytesRead) {
311 | if( bytesRead > 0 ) {
312 | self.send(chunk);
313 | pos += bytesRead;
314 | }
315 |
316 | // if we didn't get our full amount of bytes then we have read all
317 | // we can. finish.
318 | if( bytesRead < Response.__bytesToRead ) {
319 | if( previousFinishOnSend ) {
320 | self.finish();
321 | }
322 | self.finishOnSend = previousFinishOnSend;
323 | self.transferEncoding = previousEncoding;
324 | p.emitSuccess();
325 | }
326 | else {
327 | posix.read(fd, Response.__bytesToRead, pos, self.transferEncoding)
328 | .addCallback(callback)
329 | .addErrback(errback);
330 | }
331 | };
332 |
333 | posix.read(fd, Response.__bytesToRead, pos, self.transferEncoding)
334 | .addCallback(callback)
335 | .addErrback(errback);
336 | })
337 | .addErrback(errback);
338 |
339 | return p;
340 | };
341 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | exports.createCustomError = function(err_name) {
4 | var custom_err = function (message, stripPoint) {
5 | this.message = message;
6 | Error.captureStackTrace(this, stripPoint);
7 | //TODO think about removing the first entry in the stack trace
8 | // which points to this line.
9 | }
10 |
11 | custom_err.prototype = {
12 | __proto__: Error.prototype,
13 | name: err_name,
14 | stack: {}
15 | };
16 |
17 | return custom_err;
18 | };
19 |
20 | exports.bind = function(){
21 | var args = Array.prototype.slice.call(arguments);
22 | var fn = args.shift();
23 | var object = args.shift();
24 | return function(){
25 | return fn.apply(object,
26 | args.concat(Array.prototype.slice.call(arguments)));
27 | };
28 | };
29 |
30 | /* Functions for translating file extensions into mime types
31 | *
32 | * based on simonw's djangode: http://github.com/simonw/djangode
33 | * with extension/mimetypes added from felixge's
34 | * node-paperboy: http://github.com/felixge/node-paperboy
35 | */
36 | var mime = exports.mime = {
37 | lookup: function(filename, fallback) {
38 | return mime.types[path.extname(filename)] || fallback || mime.default_type;
39 | },
40 |
41 | default_type: 'application/octet-stream',
42 |
43 | types: {
44 | ".3gp" : "video/3gpp",
45 | ".a" : "application/octet-stream",
46 | ".ai" : "application/postscript",
47 | ".aif" : "audio/x-aiff",
48 | ".aiff" : "audio/x-aiff",
49 | ".arj" : "application/x-arj-compressed",
50 | ".asc" : "application/pgp-signature",
51 | ".asf" : "video/x-ms-asf",
52 | ".asm" : "text/x-asm",
53 | ".asx" : "video/x-ms-asf",
54 | ".atom" : "application/atom+xml",
55 | ".au" : "audio/basic",
56 | ".avi" : "video/x-msvideo",
57 | ".bat" : "application/x-msdownload",
58 | ".bcpio" : "application/x-bcpio",
59 | ".bin" : "application/octet-stream",
60 | ".bmp" : "image/bmp",
61 | ".bz2" : "application/x-bzip2",
62 | ".c" : "text/x-c",
63 | ".cab" : "application/vnd.ms-cab-compressed",
64 | ".cc" : "text/x-c",
65 | ".ccad" : "application/clariscad",
66 | ".chm" : "application/vnd.ms-htmlhelp",
67 | ".class" : "application/octet-stream",
68 | ".cod" : "application/vnd.rim.cod",
69 | ".com" : "application/x-msdownload",
70 | ".conf" : "text/plain",
71 | ".cpio" : "application/x-cpio",
72 | ".cpp" : "text/x-c",
73 | ".cpt" : "application/mac-compactpro",
74 | ".crt" : "application/x-x509-ca-cert",
75 | ".csh" : "application/x-csh",
76 | ".css" : "text/css",
77 | ".csv" : "text/csv",
78 | ".cxx" : "text/x-c",
79 | ".deb" : "application/x-debian-package",
80 | ".der" : "application/x-x509-ca-cert",
81 | ".diff" : "text/x-diff",
82 | ".djv" : "image/vnd.djvu",
83 | ".djvu" : "image/vnd.djvu",
84 | ".dl" : "video/dl",
85 | ".dll" : "application/x-msdownload",
86 | ".dmg" : "application/octet-stream",
87 | ".doc" : "application/msword",
88 | ".dot" : "application/msword",
89 | ".drw" : "application/drafting",
90 | ".dtd" : "application/xml-dtd",
91 | ".dvi" : "application/x-dvi",
92 | ".dwg" : "application/acad",
93 | ".dxf" : "application/dxf",
94 | ".dxr" : "application/x-director",
95 | ".ear" : "application/java-archive",
96 | ".eml" : "message/rfc822",
97 | ".eps" : "application/postscript",
98 | ".etx" : "text/x-setext",
99 | ".exe" : "application/x-msdownload",
100 | ".ez" : "application/andrew-inset",
101 | ".f" : "text/x-fortran",
102 | ".f77" : "text/x-fortran",
103 | ".f90" : "text/x-fortran",
104 | ".fli" : "video/x-fli",
105 | ".flv" : "video/x-flv",
106 | ".flv" : "video/x-flv",
107 | ".for" : "text/x-fortran",
108 | ".gem" : "application/octet-stream",
109 | ".gemspec": "text/x-script.ruby",
110 | ".gif" : "image/gif",
111 | ".gif" : "image/gif",
112 | ".gl" : "video/gl",
113 | ".gtar" : "application/x-gtar",
114 | ".gz" : "application/x-gzip",
115 | ".gz" : "application/x-gzip",
116 | ".h" : "text/x-c",
117 | ".hdf" : "application/x-hdf",
118 | ".hh" : "text/x-c",
119 | ".hqx" : "application/mac-binhex40",
120 | ".htm" : "text/html",
121 | ".html" : "text/html",
122 | ".ice" : "x-conference/x-cooltalk",
123 | ".ico" : "image/vnd.microsoft.icon",
124 | ".ics" : "text/calendar",
125 | ".ief" : "image/ief",
126 | ".ifb" : "text/calendar",
127 | ".igs" : "model/iges",
128 | ".ips" : "application/x-ipscript",
129 | ".ipx" : "application/x-ipix",
130 | ".iso" : "application/octet-stream",
131 | ".jad" : "text/vnd.sun.j2me.app-descriptor",
132 | ".jar" : "application/java-archive",
133 | ".java" : "text/x-java-source",
134 | ".jnlp" : "application/x-java-jnlp-file",
135 | ".jpeg" : "image/jpeg",
136 | ".jpg" : "image/jpeg",
137 | ".js" : "application/javascript",
138 | ".json" : "application/json",
139 | ".latex" : "application/x-latex",
140 | ".log" : "text/plain",
141 | ".lsp" : "application/x-lisp",
142 | ".lzh" : "application/octet-stream",
143 | ".m" : "text/plain",
144 | ".m3u" : "audio/x-mpegurl",
145 | ".m4v" : "video/mp4",
146 | ".man" : "text/troff",
147 | ".mathml" : "application/mathml+xml",
148 | ".mbox" : "application/mbox",
149 | ".mdoc" : "text/troff",
150 | ".me" : "text/troff",
151 | ".me" : "application/x-troff-me",
152 | ".mid" : "audio/midi",
153 | ".midi" : "audio/midi",
154 | ".mif" : "application/x-mif",
155 | ".mime" : "www/mime",
156 | ".mml" : "application/mathml+xml",
157 | ".mng" : "video/x-mng",
158 | ".mov" : "video/quicktime",
159 | ".movie" : "video/x-sgi-movie",
160 | ".mp3" : "audio/mpeg",
161 | ".mp4" : "video/mp4",
162 | ".mp4v" : "video/mp4",
163 | ".mpeg" : "video/mpeg",
164 | ".mpg" : "video/mpeg",
165 | ".mpga" : "audio/mpeg",
166 | ".ms" : "text/troff",
167 | ".msi" : "application/x-msdownload",
168 | ".nc" : "application/x-netcdf",
169 | ".oda" : "application/oda",
170 | ".odp" : "application/vnd.oasis.opendocument.presentation",
171 | ".ods" : "application/vnd.oasis.opendocument.spreadsheet",
172 | ".odt" : "application/vnd.oasis.opendocument.text",
173 | ".ogg" : "application/ogg",
174 | ".ogm" : "application/ogg",
175 | ".p" : "text/x-pascal",
176 | ".pas" : "text/x-pascal",
177 | ".pbm" : "image/x-portable-bitmap",
178 | ".pdf" : "application/pdf",
179 | ".pem" : "application/x-x509-ca-cert",
180 | ".pgm" : "image/x-portable-graymap",
181 | ".pgn" : "application/x-chess-pgn",
182 | ".pgp" : "application/pgp",
183 | ".pkg" : "application/octet-stream",
184 | ".pl" : "text/x-script.perl",
185 | ".pm" : "application/x-perl",
186 | ".png" : "image/png",
187 | ".pnm" : "image/x-portable-anymap",
188 | ".ppm" : "image/x-portable-pixmap",
189 | ".pps" : "application/vnd.ms-powerpoint",
190 | ".ppt" : "application/vnd.ms-powerpoint",
191 | ".ppz" : "application/vnd.ms-powerpoint",
192 | ".pre" : "application/x-freelance",
193 | ".prt" : "application/pro_eng",
194 | ".ps" : "application/postscript",
195 | ".psd" : "image/vnd.adobe.photoshop",
196 | ".py" : "text/x-script.python",
197 | ".qt" : "video/quicktime",
198 | ".ra" : "audio/x-realaudio",
199 | ".rake" : "text/x-script.ruby",
200 | ".ram" : "audio/x-pn-realaudio",
201 | ".rar" : "application/x-rar-compressed",
202 | ".ras" : "image/x-cmu-raster",
203 | ".rb" : "text/x-script.ruby",
204 | ".rdf" : "application/rdf+xml",
205 | ".rgb" : "image/x-rgb",
206 | ".rm" : "audio/x-pn-realaudio",
207 | ".roff" : "text/troff",
208 | ".rpm" : "application/x-redhat-package-manager",
209 | ".rpm" : "audio/x-pn-realaudio-plugin",
210 | ".rss" : "application/rss+xml",
211 | ".rtf" : "text/rtf",
212 | ".rtx" : "text/richtext",
213 | ".ru" : "text/x-script.ruby",
214 | ".s" : "text/x-asm",
215 | ".scm" : "application/x-lotusscreencam",
216 | ".set" : "application/set",
217 | ".sgm" : "text/sgml",
218 | ".sgml" : "text/sgml",
219 | ".sh" : "application/x-sh",
220 | ".shar" : "application/x-shar",
221 | ".sig" : "application/pgp-signature",
222 | ".silo" : "model/mesh",
223 | ".sit" : "application/x-stuffit",
224 | ".skt" : "application/x-koan",
225 | ".smil" : "application/smil",
226 | ".snd" : "audio/basic",
227 | ".so" : "application/octet-stream",
228 | ".sol" : "application/solids",
229 | ".spl" : "application/x-futuresplash",
230 | ".src" : "application/x-wais-source",
231 | ".stl" : "application/SLA",
232 | ".stp" : "application/STEP",
233 | ".sv4cpio" : "application/x-sv4cpio",
234 | ".sv4crc" : "application/x-sv4crc",
235 | ".svg" : "image/svg+xml",
236 | ".svgz" : "image/svg+xml",
237 | ".swf" : "application/x-shockwave-flash",
238 | ".t" : "text/troff",
239 | ".tar" : "application/x-tar",
240 | ".tbz" : "application/x-bzip-compressed-tar",
241 | ".tcl" : "application/x-tcl",
242 | ".tex" : "application/x-tex",
243 | ".texi" : "application/x-texinfo",
244 | ".texinfo" : "application/x-texinfo",
245 | ".text" : "text/plain",
246 | ".tgz" : "application/x-tar-gz",
247 | ".tif" : "image/tiff",
248 | ".tiff" : "image/tiff",
249 | ".torrent" : "application/x-bittorrent",
250 | ".tr" : "text/troff",
251 | ".tsi" : "audio/TSP-audio",
252 | ".tsp" : "application/dsptype",
253 | ".tsv" : "text/tab-separated-values",
254 | ".txt" : "text/plain",
255 | ".unv" : "application/i-deas",
256 | ".ustar" : "application/x-ustar",
257 | ".vcd" : "application/x-cdlink",
258 | ".vcf" : "text/x-vcard",
259 | ".vcs" : "text/x-vcalendar",
260 | ".vda" : "application/vda",
261 | ".vivo" : "video/vnd.vivo",
262 | ".vrm" : "x-world/x-vrml",
263 | ".vrml" : "model/vrml",
264 | ".war" : "application/java-archive",
265 | ".wav" : "audio/x-wav",
266 | ".wax" : "audio/x-ms-wax",
267 | ".wma" : "audio/x-ms-wma",
268 | ".wmv" : "video/x-ms-wmv",
269 | ".wmx" : "video/x-ms-wmx",
270 | ".wrl" : "model/vrml",
271 | ".wsdl" : "application/wsdl+xml",
272 | ".wvx" : "video/x-ms-wvx",
273 | ".xbm" : "image/x-xbitmap",
274 | ".xhtml" : "application/xhtml+xml",
275 | ".xls" : "application/vnd.ms-excel",
276 | ".xlw" : "application/vnd.ms-excel",
277 | ".xml" : "application/xml",
278 | ".xpm" : "image/x-xpixmap",
279 | ".xsl" : "application/xml",
280 | ".xslt" : "application/xslt+xml",
281 | ".xwd" : "image/x-xwindowdump",
282 | ".xyz" : "chemical/x-pdb",
283 | ".yaml" : "text/yaml",
284 | ".yml" : "text/yaml",
285 | ".zip" : "application/zip"
286 | }
287 | };
288 |
289 | var charsets = exports.charsets = {
290 | lookup: function(mimetype, fallback) {
291 | return charsets.sets[mimetype] || fallback;
292 | },
293 |
294 | sets: {
295 | "text/calendar": "UTF-8",
296 | "text/css": "UTF-8",
297 | "text/csv": "UTF-8",
298 | "text/html": "UTF-8",
299 | "text/plain": "UTF-8",
300 | "text/rtf": "UTF-8",
301 | "text/richtext": "UTF-8",
302 | "text/sgml": "UTF-8",
303 | "text/tab-separated-values": "UTF-8",
304 | "text/troff": "UTF-8",
305 | "text/x-asm": "UTF-8",
306 | "text/x-c": "UTF-8",
307 | "text/x-diff": "UTF-8",
308 | "text/x-fortran": "UTF-8",
309 | "text/x-java-source": "UTF-8",
310 | "text/x-pascal": "UTF-8",
311 | "text/x-script.perl": "UTF-8",
312 | "text/x-script.perl-module": "UTF-8",
313 | "text/x-script.python": "UTF-8",
314 | "text/x-script.ruby": "UTF-8",
315 | "text/x-setext": "UTF-8",
316 | "text/vnd.sun.j2me.app-descriptor": "UTF-8",
317 | "text/x-vcalendar": "UTF-8",
318 | "text/x-vcard": "UTF-8",
319 | "text/yaml": "UTF-8"
320 | }
321 | };
322 |
323 | // I'm going to submit a variation of this function as a patch to Node,
324 | // but the require code is in a bit of a state of flux right now.
325 | // What this function does is tell you the location of a module that has
326 | // been required. It is pretty dump at this point. I am sure there are
327 | // some edge cases I haven't caught.
328 | exports.require_resolve = function require_resolve(module_name) {
329 | var posix = require('posix');
330 |
331 | suffixes = ['.js','.node','/index.js', 'index.node'];
332 | for( var i=0; i < require.paths.length; i++ ) {
333 | var p = require.paths[i];
334 |
335 | var stat = null;
336 | for( var j=0; j < suffixes.length; j++ ) {
337 | try {
338 | stat = posix.stat(path.join(p,module_name+suffixes[j])).wait();
339 | break;
340 | }
341 | catch(err) {
342 | if( err.message != "No such file or directory" ) {
343 | throw err;
344 | }
345 | }
346 | };
347 |
348 | if( stat !== null ) {
349 | return path.join(p,module_name);
350 | }
351 | };
352 | };
353 |
--------------------------------------------------------------------------------