├── website ├── _includes │ ├── analytics.txt │ └── analytics-server.txt ├── _config.yml ├── assets │ ├── logo.png │ ├── favicon.png │ ├── ie_html5_fix.js │ └── style.css ├── docs │ ├── router.markdown │ ├── server.markdown │ ├── request.markdown │ ├── apps.markdown │ ├── bomber.markdown │ ├── response.markdown │ ├── index.markdown │ └── action.markdown ├── _layouts │ ├── page.html │ ├── docs.html │ ├── post.html │ └── default.html ├── _custom-jekyll.rb ├── _extensions │ ├── mtimeinget.rb │ ├── filters.rb │ └── regroup.rb ├── atom.xml └── index.markdown ├── .gitignore ├── test ├── fixtures │ └── testApp │ │ ├── resources │ │ ├── file.txt │ │ └── image.png │ │ ├── apps │ │ └── subApp1 │ │ │ ├── views │ │ │ └── subApp1view1.js │ │ │ └── routes.js │ │ ├── routes.js │ │ ├── views │ │ ├── view1.js │ │ └── cookie-tests.js │ │ └── config.js ├── mocks │ ├── request.js │ └── response.js ├── test-utils.js ├── test-request.js ├── test-http_responses.js ├── test-response.js ├── test-router.js ├── test-cookies.js ├── test-action.js └── test-app.js ├── exampleProject ├── resources │ ├── image.png │ └── text.txt ├── routes.js ├── config.js └── views │ └── simple.js ├── dependencies ├── node-httpclient │ ├── CHANGELOG │ ├── LICENSE │ ├── example.js │ ├── tests.js │ ├── README │ └── lib │ │ └── httpclient.js ├── node-async-testing │ ├── examples │ │ ├── tests.js │ │ └── suite.js │ ├── README │ └── async_testing.js └── sha1.js ├── lib ├── tasks │ ├── start-server.js │ └── run-tests.js ├── request.js ├── http_responses.js ├── action.js ├── server.js ├── promise.js ├── router.js ├── cookies.js ├── session.js ├── store.js ├── response.js └── utils.js ├── LICENSE ├── README.markdown └── bomber.js /website/_includes/analytics.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | website/_site 2 | exampleProject/storage/ -------------------------------------------------------------------------------- /test/fixtures/testApp/resources/file.txt: -------------------------------------------------------------------------------- 1 | text 2 | -------------------------------------------------------------------------------- /exampleProject/resources/image.png: -------------------------------------------------------------------------------- 1 | ../../website/assets/logo.png -------------------------------------------------------------------------------- /test/fixtures/testApp/resources/image.png: -------------------------------------------------------------------------------- 1 | this is a fake image 2 | -------------------------------------------------------------------------------- /website/_config.yml: -------------------------------------------------------------------------------- 1 | pygments: true 2 | permalink: /:year-:month-:day/:title.html 3 | -------------------------------------------------------------------------------- /website/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obt/bomberjs/HEAD/website/assets/logo.png -------------------------------------------------------------------------------- /website/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obt/bomberjs/HEAD/website/assets/favicon.png -------------------------------------------------------------------------------- /website/assets/ie_html5_fix.js: -------------------------------------------------------------------------------- 1 | var html5elements = ['article','section','video']; 2 | for( var i = 0; i < html5elements.length; i++ ) 3 | { 4 | document.createElement(html5elements[i]); 5 | } 6 | -------------------------------------------------------------------------------- /dependencies/node-httpclient/CHANGELOG: -------------------------------------------------------------------------------- 1 | 2010.02.04, Version 0.0.2 2 | * Merged with Benjamin Thomas changes (Benjamin Thomas) 3 | 4 | 2010.02.02, Version 0.0.1 5 | * Initial Commit (Andrew Johnston) 6 | -------------------------------------------------------------------------------- /test/fixtures/testApp/apps/subApp1/views/subApp1view1.js: -------------------------------------------------------------------------------- 1 | // a test depends on this view only having one action 2 | 3 | exports.action = function(request, response) { 4 | return "subapp1 view1 action"; 5 | }; 6 | -------------------------------------------------------------------------------- /website/docs/router.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Router 4 | --- 5 | 6 | At this time the best documentation for routing is [this blog post](http://benjaminthomas.org/2009-11-24/bomber-routing.html). 7 | -------------------------------------------------------------------------------- /test/fixtures/testApp/apps/subApp1/routes.js: -------------------------------------------------------------------------------- 1 | var Router = require('bomberjs/lib/router').Router; 2 | 3 | var r = new Router(); 4 | 5 | r.add('/:view/:action/:id'); 6 | r.add('/:view/:action'); 7 | 8 | exports.router = r; 9 | -------------------------------------------------------------------------------- /test/fixtures/testApp/routes.js: -------------------------------------------------------------------------------- 1 | var Router = require('bomberjs/lib/router').Router; 2 | 3 | var r = new Router(); 4 | 5 | r.add('/deferToSubApp1', 'subApp1'); 6 | r.add('/:view/:action'); 7 | r.addFolder(); 8 | 9 | exports.router = r; 10 | -------------------------------------------------------------------------------- /website/_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | 6 |
7 |

{{ page.title }}

8 |
9 | 10 | {{ content }} 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/_layouts/docs.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | doc_nav: true 4 | --- 5 | 6 | 7 |
8 |

{{ page.title }}

9 |
10 | 11 | {{ content }} 12 | 13 | 14 | -------------------------------------------------------------------------------- /website/docs/server.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Server 4 | --- 5 | 6 | There are no methods for interaction with a `Server` object right now. At present 7 | this really isn't meant to be used on its own, but is used by the [bomber.js](http://bomber.obtdev.local/docs/bomber.html) 8 | script for starting a Bomber server. 9 | -------------------------------------------------------------------------------- /website/docs/request.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Request 4 | --- 5 | 6 | This currently does nothing. The plan is to make it wrap the functionallity of 7 | Node's `http.serverRequest` object but adding in goodies like 8 | 9 | + Waiting for and parsing POST data 10 | + Accessing Cookies 11 | + Session support 12 | -------------------------------------------------------------------------------- /test/fixtures/testApp/views/view1.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | 3 | exports.index = function(request, response) { 4 | return "index action"; 5 | }; 6 | exports.show = function(request, response) { 7 | if( request.params.format == 'json' ) { 8 | return {a: 1, b: 'two', c: { value: 'three'}}; 9 | } 10 | else { 11 | return "show action"; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/testApp/config.js: -------------------------------------------------------------------------------- 1 | 2 | exports.project_config = { 3 | server: { 4 | port: 8342 5 | }, 6 | security: { 7 | signing_secret: 'secret' 8 | } 9 | }; 10 | 11 | exports.apps_config = { 12 | '.': { 13 | option_one: 1, 14 | option_two: 2 15 | }, 16 | 17 | 'subApp1': { 18 | option: false 19 | }, 20 | 21 | './subApp1': { 22 | option: true 23 | } 24 | }; 25 | 26 | exports.apps = [ 27 | "./apps/subApp1" 28 | ]; 29 | 30 | -------------------------------------------------------------------------------- /website/_includes/analytics-server.txt: -------------------------------------------------------------------------------- 1 | 5 | 10 | -------------------------------------------------------------------------------- /lib/tasks/start-server.js: -------------------------------------------------------------------------------- 1 | var BomberServer = require('bomberjs/lib/server').Server; 2 | 3 | exports.task = function(project, argv) { 4 | // parse arguments 5 | var opts = {}; 6 | while( argv.length > 0 ) { 7 | var stop = false; 8 | switch(argv[0]) { 9 | case "--port": 10 | case "-p": 11 | opts['port'] = argv[1]; 12 | argv.splice(0,1); 13 | break; 14 | } 15 | argv.splice(0,1); 16 | } 17 | 18 | // create and start server 19 | var bs = new BomberServer(project, opts); 20 | bs.start(); 21 | }; 22 | -------------------------------------------------------------------------------- /website/_custom-jekyll.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | 5 | version = ">= 0" 6 | 7 | if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then 8 | version = $1 9 | ARGV.shift 10 | end 11 | 12 | require 'jekyll' 13 | # Dynamically add other tags on a per site basis 14 | if File.directory?('_extensions') 15 | Dir.chdir('_extensions') do 16 | Dir['*.rb'].each do |file| 17 | require file 18 | end 19 | end 20 | end 21 | 22 | gem 'mojombo-jekyll', version 23 | load Gem.bin_path('mojombo-jekyll', 'jekyll', version) 24 | -------------------------------------------------------------------------------- /exampleProject/routes.js: -------------------------------------------------------------------------------- 1 | var Router = require('bomberjs/lib/router').Router; 2 | 3 | var r = new Router(); 4 | 5 | r.add('/', { view: 'simple', action: 'index' }); 6 | r.add('/env', { view: 'simple', action: 'env' }); 7 | 8 | // will return a 500 error as the view doesn't exist 9 | r.add('/section', { view: 'simple', action: 'section' }) 10 | 11 | r.add('/section/:id', { view: 'simple', action: 'show', id: '[0-9]+' }) 12 | 13 | r.addFolder(); 14 | r.addFolder({path:'/photos/'}); 15 | 16 | // default catch all routes 17 | r.add('/:view/:action/:id'); 18 | r.add('/:view/:action'); 19 | 20 | exports.router = r; 21 | -------------------------------------------------------------------------------- /website/_extensions/mtimeinget.rb: -------------------------------------------------------------------------------- 1 | module BTorg 2 | 3 | class MtimeInGet < Liquid::Tag 4 | def initialize(tag_name, markup, tokens) 5 | super 6 | unless markup.nil? or markup.empty? 7 | @filename = markup.strip 8 | else 9 | raise SyntaxError.new("Syntax Error in 'mtimeinget' - Valid syntax: mtimeinget ") 10 | end 11 | end 12 | 13 | def render(context) 14 | f = File::Stat.new context['site']['source']+@filename 15 | return @filename + '?' + f.mtime.to_i.to_s 16 | end 17 | end 18 | end 19 | 20 | Liquid::Template.register_tag('mtimeinget', BTorg::MtimeInGet) 21 | -------------------------------------------------------------------------------- /website/docs/apps.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: App Structure 4 | --- 5 | 6 | Apps are just a bunch of functionallity wrapped up into a folder. Bomber 7 | will look for the following files/folders (all of them are optional): 8 | 9 | + `config.js` 10 | This stores configuration for the app. A [Server](/docs/server.html) will look for the following 11 | exports: 12 | 13 | `apps` 14 | : an array of apps that this app depends on. The Server preloads all apps it is going to need. 15 | 16 | + `routes.js` 17 | should export a [Router object](/docs/router.html) 18 | 19 | + `views/.js` 20 | a view file exports functions (called actions to steal from Rails) that will 21 | be called on an [Action object](/docs/action.html) 22 | -------------------------------------------------------------------------------- /website/docs/bomber.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: bomber.js 4 | --- 5 | 6 | `bomber.js` is a shell script that is used to manage your Bomber projects. 7 | 8 | Right now all it can do is start a Bomber [Server](/docs/server.html). 9 | 10 | It takes one optional argument, the name or the path of a Bomber app. This argument is 11 | used by the Node `require` command, so [read up on how that works](http://nodejs.org/api.html#_modules) 12 | to make sure Bomber will be able to find your app. If the argument isn't supplied it 13 | uses the current directory. 14 | 15 | Examples 16 | -------- 17 | 18 | {% highlight sh %} 19 | ./bomber.js server 20 | {% endhighlight %} 21 | 22 | {% highlight sh %} 23 | ./bomber.js --app ./exampleProject server 24 | {% endhighlight %} 25 | -------------------------------------------------------------------------------- /website/_extensions/filters.rb: -------------------------------------------------------------------------------- 1 | require 'hpricot' 2 | require 'cgi' 3 | 4 | module BTorg 5 | 6 | module Filters 7 | def html_truncatewords input, words = 15, truncate_string = "..." 8 | doc = Hpricot.parse(input) 9 | (doc/:"text()").to_s.split[0..words].join(' ') + truncate_string 10 | end 11 | 12 | def css_path_to_ids input 13 | html = Hpricot.parse(input) 14 | 15 | html.search("p,h1,h2,h3,h4,h5,h6,ul,dl,div,blockquote,table").each do |p| 16 | p.set_attribute('id', p.css_path.gsub(/\s/,'').gsub(/[^a-zA-Z0-9_]/,'_').gsub(/(^_+|_+$)/,'')) 17 | end 18 | 19 | html 20 | end 21 | 22 | def to_i x 23 | x.to_i 24 | end 25 | 26 | def html_escape x 27 | CGI.escape(x) 28 | end 29 | end 30 | end 31 | 32 | Liquid::Template.register_filter(BTorg::Filters) 33 | -------------------------------------------------------------------------------- /dependencies/node-async-testing/examples/tests.js: -------------------------------------------------------------------------------- 1 | var test = require('../async_testing').test; 2 | 3 | test("this does something", function(test) { 4 | test.assert.ok(true); 5 | test.finish(); 6 | }); 7 | test("this doesn't fail", function(test) { 8 | test.assert.ok(true); 9 | setTimeout(function() { 10 | test.assert.ok(true); 11 | test.finish(); 12 | }, 1000); 13 | }); 14 | test("this does something else", function(test) { 15 | test.assert.ok(true); 16 | test.assert.ok(true); 17 | test.finish(); 18 | }); 19 | test("this fails", function(test) { 20 | setTimeout(function() { 21 | test.assert.ok(false); 22 | test.finish(); 23 | }, 1000); 24 | }); 25 | test("throws", function(test) { 26 | test.assert.throws(function() { 27 | throw new Error(); 28 | }); 29 | test.finish(); 30 | }); 31 | -------------------------------------------------------------------------------- /website/_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 |
7 |

{{ page.title }}

8 |

9 |
10 | 11 | {{ content|css_path_to_ids }} 12 |
13 | 14 |
15 |

Have something to say in response to this post?

16 |

Send me an email or Tweet about it.

17 |
18 | 19 | {% if site.related_posts %} 20 |
21 |

Possibly related entries

22 | 23 |
24 | {% for post in site.related_posts limit:3 %} 25 |
{{ post.date | date: "%b %Y" }}
26 |
{{ post.title }}
27 | {% endfor %} 28 |
29 |
30 | 31 | {% endif %} 32 | -------------------------------------------------------------------------------- /test/mocks/request.js: -------------------------------------------------------------------------------- 1 | /* Mock Node http.serverRequest 2 | * 3 | * Responds to all the methods that http.serverRequest does (or it will 4 | * eventually). 5 | */ 6 | 7 | var MockRequest = exports.MockRequest = function(method, url) { 8 | this.method = method; 9 | this.url = url; 10 | this.headers = null; 11 | this.body = []; 12 | this.finished = false; 13 | this.httpVersion = "1.1"; 14 | 15 | this._listeners = {}; 16 | }; 17 | 18 | MockRequest.prototype.addListener = function(event, callback) { 19 | if( !(event in this._listeners) ) { 20 | this._listeners[event] = []; 21 | } 22 | 23 | this._listeners[event].push(callback); 24 | }; 25 | 26 | MockRequest.prototype.emit = function() { 27 | var args = Array.prototype.slice.call(arguments); 28 | var event = args.shift(); 29 | 30 | this._listeners[event].forEach(function(cb) { 31 | cb.apply(null, args); 32 | }); 33 | }; 34 | 35 | /* Not implemented: 36 | * 37 | * + setBodyEncoding 38 | * + pause 39 | * + resume 40 | * + connection 41 | */ 42 | -------------------------------------------------------------------------------- /website/atom.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: none 3 | --- 4 | 5 | 6 | 7 | News for Bomber, a node.js web framework 8 | 9 | 10 | {{ site.time | date_to_xmlschema }} 11 | http://bomber.obtdev.com/ 12 | 13 | Benjamin Thomas or Omer Bar-or 14 | admin@obtdev.com 15 | 16 | 17 | {% for post in site.posts limit:7 %} 18 | 19 | {% if post.author %} 20 | 21 | {{ post.author }} 22 | {% if post.email %}{{ post.email }}{% endif %} 23 | 24 | {% endif %} 25 | {{ post.title }} 26 | 27 | {{ post.date | date_to_xmlschema }} 28 | tag:bomber.obtdev.com,2009-10-22:{{ post.id }} 29 | {{ post.content | xml_escape }} 30 | 31 | 32 | {% endfor %} 33 | 34 | -------------------------------------------------------------------------------- /test/mocks/response.js: -------------------------------------------------------------------------------- 1 | /* Mock Node http.serverResponse 2 | * 3 | * Responds to all the methods that http.serverResponse does, and basically just 4 | * stores everything it is told. 5 | * 6 | * The idea being that you can then make assertions about what it has been told 7 | * to make sure your code is working properly. 8 | */ 9 | 10 | var MockResponse = function() { 11 | this.status = null; 12 | this.headers = null; 13 | this.body = []; 14 | this.bodyText = ''; 15 | this.finished = false; 16 | }; 17 | MockResponse.prototype.sendHeader = function(status, headers) { 18 | if( this.finished ) { 19 | throw "Already finished"; 20 | } 21 | this.status = status; 22 | this.headers = headers; 23 | }; 24 | MockResponse.prototype.sendBody = function(chunk, encoding) { 25 | if( this.finished ) { 26 | throw "Already finished"; 27 | } 28 | this.body.push([chunk, encoding]); 29 | this.bodyText += chunk; 30 | }; 31 | MockResponse.prototype.finish = function() { 32 | if( this.finished ) { 33 | throw "Already finished"; 34 | } 35 | this.finished = true; 36 | }; 37 | 38 | exports.MockResponse = MockResponse; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Benjamin Thomas and Omer Bar-or 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /dependencies/node-httpclient/LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright 2010, Andrew Johnston All rights reserved. 2 | // Permission is hereby granted, free of charge, to any person obtaining a copy 3 | // of this software and associated documentation files (the "Software"), to 4 | // deal in the Software without restriction, including without limitation the 5 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | // sell copies of the Software, and to permit persons to whom the Software is 7 | // furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in 10 | // all copies or substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | // IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /dependencies/node-httpclient/example.js: -------------------------------------------------------------------------------- 1 | var sys = require("sys"); 2 | var httpcli = require("./lib/httpclient"); 3 | 4 | var url = "http://www.betfair.com"; 5 | var surl = "https://www.betfair.com"; 6 | 7 | function verifyTLS(request) { 8 | sys.puts(sys.inspect(request)); 9 | return true; 10 | } 11 | 12 | var client = new httpcli.httpclient(); 13 | 14 | // a simple http request with default options (gzip off, keepalive off, https off) 15 | client.perform(url, "GET", function(result) { 16 | sys.puts(sys.inspect(result)); 17 | }); 18 | 19 | var client2 = new httpcli.httpclient(); 20 | 21 | // nested calls with gzip compression and keep-alive 22 | client2.perform(url, "GET", function(result) { 23 | sys.puts(sys.inspect(result)); 24 | client2.perform(url, "GET", function(result) { 25 | sys.puts(sys.inspect(result)); 26 | // https request with callback handling of certificate validation 27 | client2.perform(surl, "GET", function(result) { 28 | sys.puts(sys.inspect(result)); 29 | }, null, {"Accept-Encoding" : "none,gzip", "Connection" : "close"}, verifyTLS); 30 | }, null, {"Accept-Encoding" : "none,gzip", "Connection" : "Keep-Alive"}); 31 | }, null, {"Accept-Encoding" : "none,gzip", "Connection" : "Keep-Alive"}); 32 | -------------------------------------------------------------------------------- /website/docs/response.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Response 4 | --- 5 | 6 | A wrapper around the Node's `http.ServerResponse` object. Right now it 7 | mostly makes sending responses and setting headers easier. 8 | 9 | The plan is to add some other niceties like: 10 | 11 | + Setting cookies 12 | + Setting session variables 13 | 14 | Public methods/variables: 15 | --------------- 16 | 17 | `setHeader(key, value)` 18 | : Pretty simple setter function. Down the line it would be nice to make this 19 | handle setting headers multiple times, and other things discussed in 20 | [this thread from the mailing list](http://groups.google.com/group/nodejs/browse_thread/thread/9f4e8763ccf1fd09#). 21 | 22 | `sendHeaders()` 23 | : Sends the headers for this request 24 | 25 | `finishOnSend` 26 | : An option for what should happen when the `send()` method is called. If `true` 27 | (the default), `send()` will call `finish()` when it is done. 28 | 29 | `send(str)` 30 | : Very similar to Node's `http.ServerResponse.send` except it will automatically 31 | send the headers first if they haven't already been sent. And unless you have 32 | set `Response.finishOnSend` to `false` for this response it will also call `finish()` 33 | 34 | `finish()` 35 | : Closes the connection to the client. 36 | -------------------------------------------------------------------------------- /website/docs/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Documentation 4 | --- 5 | 6 | We just started writing Bomber, so the API is very much in a state of 7 | flux. Because of this the best documentation is going to be the source 8 | code itself. I have tried to be diligent about making sure to comment. 9 | So, when in doubt consult the source. 10 | 11 | The Documentation consists of the following sub-sections: 12 | 13 | Overviews 14 | --------- 15 | 16 | [bomber.js](/docs/bomber.html) 17 | : `bomber.js` is the script you will use to manage Bomber projects 18 | 19 | [App Structure](/docs/apps.html) 20 | : A summary of what files an App can have 21 | 22 | API 23 | --- 24 | 25 | [Action](/docs/action.html) 26 | : Action objects are where you generate your responses. 27 | 28 | [Request](/docs/request.html) 29 | : A wrapper around the node.js `http.ServerRequest` object. Makes it easier 30 | to do things like wait for and parse POST data. 31 | 32 | [Response](/docs/response.html) 33 | : A wrapper around the node.js `http.ServerResponse` object. Adds some 34 | niceties. 35 | 36 | [Router](/docs/router.html) 37 | : A Router is used to turn a URL into an action. 38 | 39 | [Server](/docs/server.html) 40 | : The server object is what manages listening for connections, and 41 | finding and calling the appropriate actions. 42 | -------------------------------------------------------------------------------- /website/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.title %}{{ page.title }} — {% endif %}Bomber: a node.js javascript web framework 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

15 | Bomber 16 |

17 |

A node.js javascript web framework

18 |
19 | 20 | 37 | 38 | {{ content }} 39 | 40 | {% include analytics.txt %} 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var TestSuite = require('../dependencies/node-async-testing/async_testing').TestSuite; 3 | 4 | var utils = require('../lib/utils'); 5 | 6 | (new TestSuite('Utils Tests')).runTests({ 7 | "test custom error": function(test) { 8 | var MyError = utils.createCustomError('MyError'); 9 | 10 | var e = new MyError('Test'); 11 | 12 | test.assert.ok(e instanceof Error); 13 | test.assert.equal('Test', e.message); 14 | var stack = e.stack.split(/\n/); 15 | test.assert.equal('MyError: Test', stack[0]); 16 | }, 17 | "test bind": function(test) { 18 | var test = this; 19 | var func = function(arg1, arg2) { 20 | test.assert.equal(test, this); 21 | test.assert.equal(1, arg1); 22 | test.assert.equal(2, arg2); 23 | }; 24 | 25 | utils.bind(func, this, 1, 2)(); 26 | }, 27 | "test mime lookup": function(test) { 28 | // easy 29 | test.assert.equal('text/plain', utils.mime.lookup('text.txt')); 30 | 31 | // just an extension 32 | test.assert.equal('text/plain', utils.mime.lookup('.txt')); 33 | 34 | // default 35 | test.assert.equal('application/octet-stream', utils.mime.lookup('text.nope')); 36 | 37 | // fallback 38 | test.assert.equal('fallback', utils.mime.lookup('text.fallback', 'fallback')); 39 | }, 40 | "test charset lookup": function(test) { 41 | // easy 42 | test.assert.equal('UTF-8', utils.charsets.lookup('text/plain')); 43 | 44 | // none 45 | test.assert.ok(typeof utils.charsets.lookup('text/nope') == 'undefined'); 46 | 47 | // fallback 48 | test.assert.equal('fallback', utils.charsets.lookup('text/fallback', 'fallback')); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /lib/tasks/run-tests.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var posix = require('posix'); 3 | var path = require('path'); 4 | 5 | var require_resolve = require('bomberjs/lib/utils').require_resolve; 6 | 7 | exports.task = function(project) { 8 | var tests = []; 9 | 10 | var path_to_app = project.base_app.module_path; 11 | 12 | if( path_to_app.charAt(0) !== '/' ) { 13 | path_to_app = require_resolve(path_to_app); 14 | } 15 | 16 | var paths_to_check = [path.join(path_to_app,'test')]; 17 | 18 | while(paths_to_check.length > 0) { 19 | var cur_path = paths_to_check.pop(); 20 | var dir = posix.readdir(cur_path).wait(); 21 | dir.forEach(function(file_name) { 22 | var stat = posix.stat(path.join(cur_path, file_name)).wait(); 23 | if( stat.isFile() ) { 24 | if( !file_name.match(/^test-.*\.js$/) ) { 25 | return; 26 | } 27 | tests.push(path.join(cur_path, file_name)); 28 | } 29 | else if( stat.isDirectory() ) { 30 | paths_to_check.push(path.join(cur_path, file_name)); 31 | } 32 | }); 33 | } 34 | 35 | function runNextTest() { 36 | if( tests.length < 1 ) { 37 | return; 38 | } 39 | var test = tests.shift(); 40 | process.createChildProcess('node',[test]) 41 | .addListener('output', function(str) { 42 | if( str !== null ) { 43 | //sys.print(str); 44 | } 45 | }) 46 | .addListener('error', function(str) { 47 | if( str !== null ) { 48 | sys.print(str); 49 | } 50 | }) 51 | .addListener('exit', function(str) { 52 | runNextTest(); 53 | }) 54 | .close(); 55 | } 56 | 57 | runNextTest(); 58 | }; 59 | -------------------------------------------------------------------------------- /exampleProject/config.js: -------------------------------------------------------------------------------- 1 | /* PROJECT CONFIGURATION */ 2 | 3 | exports.project_config = { 4 | server: { 5 | // Port to run node server on (default 8400) 6 | //port: 8000, 7 | sessions: { 8 | // How do you want to store the content of sessions 9 | // Options are: 10 | // 'disk' [default]: Manage content on disk. You can control the location of 11 | // the on-disk storage in the disk_storage_location parameter. 12 | // 'cookies': Store in signed cookies. Cookies are a quick and fairly secure 13 | // way to store simple session values, but complex data probably shouldn't be 14 | // handled with cookies. Also, stuff written to the session after the request 15 | // headers have been sent won't be saved. 16 | storage_method: 'disk', 17 | 18 | // Directory for on-disk storage of sessions 19 | // Default: '/tmp/' 20 | disk_storage_location: './storage/', 21 | 22 | // Minutes before sessions expire 23 | // Default: 600 (10 hours) 24 | expire_minutes: 600, 25 | 26 | // Minutes before a session is renewed 27 | // Default: 10 (10 minutes) 28 | renew_minutes: 10, 29 | 30 | cookie: { 31 | // Name of the cookie to use for session 32 | // Default: 'session_key' 33 | name: 'session_key', 34 | 35 | // Domain for the session cookie 36 | // Default: '' 37 | domain: '', 38 | 39 | // Path for the session cookie 40 | // Default: '/' 41 | path: '/', 42 | 43 | // Whether to require session cookies only to be sent over secure connections (Boolean) 44 | // Default: false 45 | secure: false 46 | } 47 | } 48 | }, 49 | 50 | security: { 51 | // A secret key to sign sessions, cookies, passwords etc. 52 | // Make sure you set this, and that you keep this secret -- well, secret 53 | signing_secret: "secret" 54 | }, 55 | 56 | }; 57 | 58 | -------------------------------------------------------------------------------- /website/_extensions/regroup.rb: -------------------------------------------------------------------------------- 1 | module BTorg 2 | 3 | class RegroupBlock < Liquid::Block 4 | include Liquid::StandardFilters 5 | # we need a language, but the linenos argument is optional. 6 | SYNTAX = /^([\w.]+) by ([\w.]+) as (\w+)/ 7 | 8 | def initialize(tag_name, markup, tokens) 9 | super 10 | if markup =~ SYNTAX 11 | @list = $1 12 | @attribute = $2 13 | @group_name = $3 14 | else 15 | raise SyntaxError.new("Syntax Error in 'regroup' - Valid syntax: regroup by as ") 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 | --------------------------------------------------------------------------------