├── .gitignore ├── examples ├── hello │ ├── package.json │ └── main.js ├── shorty.js ├── chat-router.js ├── upload.js ├── statuscodedrinkinggame.js ├── async.js ├── comet.js └── example.js ├── bin ├── jackup.cmd ├── jackup ├── all-tests.cmd ├── cgi-test.sh └── fcgi-test.sh ├── tests ├── jack │ ├── multipart │ │ ├── file1.txt │ │ ├── README │ │ ├── binary │ │ ├── ie │ │ ├── none │ │ ├── empty │ │ ├── text │ │ └── nested │ ├── fixtures │ │ └── rack-logo.jpg │ ├── all-tests.js │ ├── auth │ │ ├── all-tests.js │ │ ├── auth-abstract-tests.js │ │ ├── auth-basic-tests.js │ │ └── auth-digest-tests.js │ ├── session-cookie-tests.js │ ├── request-tests.js │ └── utils-tests.js └── jack-tests.js ├── support ├── servlet │ └── README └── v8cgi │ └── jack.ssjs ├── jars └── simple-4.1.10.jar ├── lib ├── jack │ ├── handler │ │ ├── fastcgi.js │ │ ├── fastcgi-rhino.js │ │ ├── v8cgi.js │ │ ├── jetty.js │ │ ├── cgi.js │ │ ├── fastcgi-k7.js │ │ ├── fastcgi-rhino-jna.js │ │ ├── simple.js │ │ ├── worker-delegator.js │ │ ├── shttpd.js │ │ ├── mozhttpd.js │ │ ├── simple-worker.js │ │ └── servlet.js │ ├── content.js │ ├── head.js │ ├── session.js │ ├── reloader.js │ ├── csrf.js │ ├── static.js │ ├── auth │ │ ├── abstract │ │ │ ├── handler.js │ │ │ └── request.js │ │ ├── digest │ │ │ ├── params.js │ │ │ ├── request.js │ │ │ ├── nonce.js │ │ │ └── handler.js │ │ └── basic │ │ │ └── handler.js │ ├── redirect.js │ ├── middleware.js │ ├── cascade.js │ ├── showexceptions.js │ ├── contenttype.js │ ├── contentlength.js │ ├── methodoverride.js │ ├── path.js │ ├── http-params.js │ ├── jsonp.js │ ├── directory.js │ ├── urlmap.js │ ├── session │ │ └── cookie.js │ ├── file.js │ ├── commonlogger.js │ ├── dir.js │ ├── mock.js │ ├── showstatus.js │ ├── querystring.js │ ├── narwhal.js │ ├── response.js │ ├── mime.js │ ├── lint.js │ └── utils.js ├── jack.js └── jackup.js ├── jackconfig.js ├── package.json ├── docs ├── jack-component-status.md ├── writing-jsgi-middleware.md ├── getting-started-with-jack.md ├── writing-jsgi-applications.md ├── jack-auth.md └── jsgi-spec.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /examples/hello/package.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/jackup.cmd: -------------------------------------------------------------------------------- 1 | narwhal.cmd %~dpn0 %* -------------------------------------------------------------------------------- /tests/jack/multipart/file1.txt: -------------------------------------------------------------------------------- 1 | contents -------------------------------------------------------------------------------- /bin/jackup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env narwhal 2 | require('jackup').main(system.args); -------------------------------------------------------------------------------- /support/servlet/README: -------------------------------------------------------------------------------- 1 | Moved to http://github.com/tlrobinson/jack-servlet/ 2 | -------------------------------------------------------------------------------- /tests/jack/multipart/README: -------------------------------------------------------------------------------- 1 | These files were borrowed from the Rack test suite. -------------------------------------------------------------------------------- /jars/simple-4.1.10.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriszyp/jack/master/jars/simple-4.1.10.jar -------------------------------------------------------------------------------- /tests/jack/multipart/binary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriszyp/jack/master/tests/jack/multipart/binary -------------------------------------------------------------------------------- /bin/all-tests.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | setlocal 4 | 5 | :: all tests 6 | narwhal .\tests\jack-tests.js 7 | 8 | -------------------------------------------------------------------------------- /tests/jack/fixtures/rack-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriszyp/jack/master/tests/jack/fixtures/rack-logo.jpg -------------------------------------------------------------------------------- /tests/jack-tests.js: -------------------------------------------------------------------------------- 1 | exports.testJack = require("./jack/all-tests"); 2 | 3 | if (require.main === module.id) 4 | require("test/runner").run(exports); 5 | -------------------------------------------------------------------------------- /lib/jack/handler/fastcgi.js: -------------------------------------------------------------------------------- 1 | var FastCGI = require("./fastcgi-"+system.engine); 2 | 3 | for (var property in FastCGI) 4 | exports[property] = FastCGI[property]; 5 | -------------------------------------------------------------------------------- /tests/jack/multipart/ie: -------------------------------------------------------------------------------- 1 | --AaB03x 2 | Content-Disposition: form-data; name="files"; filename="C:\Documents and Settings\Administrator\Desktop\file1.txt" 3 | Content-Type: text/plain 4 | 5 | contents 6 | --AaB03x-- 7 | -------------------------------------------------------------------------------- /tests/jack/multipart/none: -------------------------------------------------------------------------------- 1 | --AaB03x 2 | Content-Disposition: form-data; name="submit-name" 3 | 4 | Larry 5 | --AaB03x 6 | Content-Disposition: form-data; name="files"; filename="" 7 | 8 | 9 | --AaB03x-- 10 | -------------------------------------------------------------------------------- /tests/jack/multipart/empty: -------------------------------------------------------------------------------- 1 | --AaB03x 2 | Content-Disposition: form-data; name="submit-name" 3 | 4 | Larry 5 | --AaB03x 6 | Content-Disposition: form-data; name="files"; filename="file1.txt" 7 | Content-Type: text/plain 8 | 9 | 10 | --AaB03x-- 11 | -------------------------------------------------------------------------------- /tests/jack/multipart/text: -------------------------------------------------------------------------------- 1 | --AaB03x 2 | Content-Disposition: form-data; name="submit-name" 3 | 4 | Larry 5 | --AaB03x 6 | Content-Disposition: form-data; name="files"; filename="file1.txt" 7 | Content-Type: text/plain 8 | 9 | contents 10 | --AaB03x-- -------------------------------------------------------------------------------- /tests/jack/multipart/nested: -------------------------------------------------------------------------------- 1 | --AaB03x 2 | Content-Disposition: form-data; name="foo[submit-name]" 3 | 4 | Larry 5 | --AaB03x 6 | Content-Disposition: form-data; name="foo[files]"; filename="file1.txt" 7 | Content-Type: text/plain 8 | 9 | contents 10 | --AaB03x-- 11 | -------------------------------------------------------------------------------- /lib/jack/content.js: -------------------------------------------------------------------------------- 1 | 2 | exports.Content = function (content, contentType) { 3 | return function (request) { 4 | return { 5 | status : 200, 6 | headers : { "content-type": contentType || "text/html" }, 7 | body : [content] 8 | }; 9 | }; 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /examples/shorty.js: -------------------------------------------------------------------------------- 1 | // a short url shortener. no persistance. 2 | exports.app=function(r){return r.pathInfo!='/'?{status:301,headers:{'location':d[r.pathInfo.substr(1)]},body:[]}:{status:200,headers:{'content-type':'text/html'},body:[r.queryString?''+(d.push(decodeURIComponent(r.queryString.substr(2)))-1):'
']}};d=[] 3 | -------------------------------------------------------------------------------- /support/v8cgi/jack.ssjs: -------------------------------------------------------------------------------- 1 | var jack = require("jack"); 2 | 3 | var app = function(env) { 4 | return { 5 | status : 200, 6 | headers : { "Content-Type" : "text/html" }, 7 | body : "hello world!" 8 | }; 9 | } 10 | 11 | app = jack.Lint(app); 12 | 13 | require("jack/handler/v8cgi").run(app, request, response); 14 | -------------------------------------------------------------------------------- /tests/jack/all-tests.js: -------------------------------------------------------------------------------- 1 | exports.testJackUtils = require("./utils-tests"); 2 | exports.testJackRequest = require("./request-tests"); 3 | exports.testJackSessionCookie = require("./session-cookie-tests"); 4 | exports.testJackAuth = require("./auth/all-tests"); 5 | 6 | if (require.main === module.id) 7 | require("test/runner").run(exports); 8 | 9 | -------------------------------------------------------------------------------- /bin/cgi-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # invokes a CGI application 4 | 5 | export SCRIPT_NAME="" 6 | export PATH_INFO="/" 7 | 8 | export REQUEST_METHOD="GET" 9 | export SERVER_NAME="localhost" 10 | export SERVER_PORT="80" 11 | export QUERY_STRING="" 12 | export SERVER_PROTOCOL="HTTP/1.1" 13 | 14 | export REMOTE_HOST="127.0.0.1" 15 | 16 | echo "Host: localhost" | $1 17 | -------------------------------------------------------------------------------- /examples/chat-router.js: -------------------------------------------------------------------------------- 1 | var ports = []; 2 | onconnect = function(event){ 3 | var port = event.port; 4 | // connect to this port, listening for any messages 5 | ports.push(port); 6 | port.onmessage = function(event){ 7 | 8 | // got a message, send it to everyone! 9 | for(var i = 0; i < ports.length; i++){ 10 | var port = ports[i]; 11 | port.postMessage(event.data); 12 | } 13 | }; 14 | }; -------------------------------------------------------------------------------- /examples/hello/main.js: -------------------------------------------------------------------------------- 1 | 2 | var ContentLength = require('jack/contentlength').ContentLength; 3 | 4 | exports.app = new ContentLength(function (env) { 5 | return { 6 | status : 200, 7 | headers : {"content-type": "text/plain"}, 8 | body : ["Hello, World!"] 9 | }; 10 | }); 11 | 12 | if (require.main == module.id) 13 | require("jackup").main(system.args.concat([module.id])); 14 | 15 | -------------------------------------------------------------------------------- /lib/jack.js: -------------------------------------------------------------------------------- 1 | var Jack = exports; 2 | 3 | Jack.Utils = require("jack/utils"); 4 | 5 | Jack.Mime = require("jack/mime"); 6 | 7 | var middleware = require("jack/middleware"); 8 | for (var name in middleware) 9 | Jack[name] = middleware[name]; 10 | 11 | Jack.Request = require("jack/request").Request; 12 | Jack.Response = require("jack/response").Response; 13 | 14 | Jack.Narwhal = require("jack/narwhal").app; 15 | -------------------------------------------------------------------------------- /tests/jack/auth/all-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | exports.testAuthAbstract = require("./auth-abstract-tests"); 11 | exports.testAuthBasic = require("./auth-basic-tests"); 12 | exports.testAuthDigest = require("./auth-digest-tests"); -------------------------------------------------------------------------------- /lib/jack/head.js: -------------------------------------------------------------------------------- 1 | var when = require("promise").whenPreservingType; 2 | 3 | var Head = exports.Head = function(nextApp) { 4 | return function(request) { 5 | if (request.method === "HEAD") 6 | request.method = "GET"; // HEAD must act the same as GET 7 | 8 | return when(nextApp(request), function(response) { 9 | if (request.method === "HEAD") 10 | response.body = []; 11 | return response; 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /lib/jack/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session variables live throughout a user's session. 3 | * 4 | * HTTP is a stateless protocol for a *good* reason. Try to avoid using 5 | * session variables. 6 | */ 7 | var Session = exports.Session = function(request) { 8 | if (!request.env.jack) request.env.jack = {}; 9 | if (!request.env.jack.session) { 10 | try { 11 | request.env.jack.session = request.env.jack.session.loadSession(request); 12 | } catch (err) { 13 | request.env.jack = {}; 14 | } 15 | } 16 | 17 | return request.env.jack.session; 18 | } 19 | -------------------------------------------------------------------------------- /lib/jack/reloader.js: -------------------------------------------------------------------------------- 1 | var Sandbox = require("sandbox").Sandbox; 2 | 3 | exports.Reloader = function(id, appName) { 4 | appName = appName || 'app'; 5 | return function(request) { 6 | var sandbox = Sandbox({ 7 | "system": system, 8 | modules: { 9 | "event-loop": require("event-loop"), 10 | "packages": require("packages") 11 | }, 12 | "loader": require.loader, 13 | "debug": require.loader.debug 14 | }); 15 | var module = sandbox(id); // not as main, key 16 | return module[appName](request); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/jack/csrf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Detects cross-site forgeable requests and warns downstream middleware/apps 3 | * by adding a crossSiteForgeable property to the request 4 | */ 5 | exports.CSRFDetect = function(nextApp, customHeader){ 6 | customHeader = customHeader || "client-id"; 7 | return function(request){ 8 | var headers = request.headers; 9 | if(!(headers[customHeader] || /application\/j/.test(headers.accept) || 10 | (request.method == "POST" && headers.referer && headers.referer.indexOf(headers.host + '/') > 0) || 11 | (request.method != "GET" && request.method != "POST"))){ 12 | request.crossSiteForgeable = true; 13 | } 14 | return nextApp(request); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /jackconfig.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env jackup 2 | 3 | // the "app" export is the default property used by jackup: 4 | exports.app = function(env) { 5 | return { 6 | status : 200, 7 | headers : {"content-type":"text/plain"}, 8 | body : ["jackconfig.js is the default file jackup looks for!"] 9 | }; 10 | } 11 | 12 | // specify custom sets of middleware and initialization routines 13 | // by defining a function with the same name as the environment: 14 | exports.development = function(app) { 15 | return require("jack/commonlogger").CommonLogger( 16 | require("jack/showexceptions").ShowExceptions( 17 | require("jack/lint").Lint( 18 | require("jack/contentlength").ContentLength(app)))); 19 | } 20 | -------------------------------------------------------------------------------- /lib/jack/handler/fastcgi-rhino.js: -------------------------------------------------------------------------------- 1 | exports.run = function(app, options) { 2 | throw "NYI"; 3 | 4 | var options = options || {}; 5 | //java.lang.System.setProperties("FCGI_PORT", options["port"] || 8080); 6 | while (true) 7 | { 8 | var result = new Packages.com.fastcgi.FCGIInterface().FCGIaccept() 9 | if (result < 0) 10 | break; 11 | 12 | serve(Packages.com.fastcgi.FCGIInterface.request, app); 13 | } 14 | } 15 | 16 | var serve = function(request, app) { 17 | print("Serving FastCGI request (if it were implememted...)"); 18 | //var env = { 19 | // "jsgi.input" : request.input, 20 | // "jsgi.errors" : request.err, 21 | // 22 | // "jsgi.multithread" : false, 23 | // "jsgi.multiprocess" : true, 24 | // "jsgi.run_once" : false, 25 | // }; 26 | } 27 | -------------------------------------------------------------------------------- /lib/jack/static.js: -------------------------------------------------------------------------------- 1 | var File = require("./file").File; 2 | 3 | var Static = exports.Static = function(app, options) { 4 | var options = options || {}, 5 | urls = options["urls"] || ["/favicon.ico"], 6 | root = options["root"] || '.', 7 | fileServer = File(root, options); 8 | 9 | return function(request) { 10 | var path = request.pathInfo; 11 | 12 | for (var i = 0; i < urls.length; i++) 13 | if (path.indexOf(urls[i]) === 0) { 14 | var result = fileServer(request, request.pathInfo.substring(urls[i].length)); 15 | return result; 16 | } 17 | if(app){ 18 | return app(request); 19 | } 20 | else{ 21 | return { 22 | status: 404, 23 | headers: {}, 24 | body: ["Not found"] 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /lib/jack/auth/abstract/handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | var update = require("hash").Hash.update, 11 | responseForStatus = require("jack/utils").responseForStatus; 12 | 13 | var Handler = exports.Handler = function(params) { 14 | if (params) update(this, params); 15 | }; 16 | 17 | Handler.prototype = { 18 | 19 | Unauthorized: function(challenge) { 20 | var response = responseForStatus(401); 21 | HashP.set(response.headers, "WWW-Authenticate", challenge || this.issueChallenge()); 22 | return response; 23 | }, 24 | 25 | BadRequest: responseForStatus(400), 26 | 27 | isValid: function() { 28 | throw "jack.auth.abstract.handler.isValid(): override required!"; 29 | } 30 | }; -------------------------------------------------------------------------------- /lib/jack/redirect.js: -------------------------------------------------------------------------------- 1 | var uri = require("uri"); 2 | 3 | exports.Redirect = function (path, status) { 4 | 5 | status = status || 301; 6 | 7 | return function (request) { 8 | var location = 9 | (request.scheme || "http") + 10 | "://" + 11 | (request.headers.host || ( 12 | request.host + 13 | (request.port == 80 ? "" : ":" + request.port) 14 | )) + 15 | (request.scriptName || "") + 16 | request.pathInfo; 17 | 18 | location = path ? uri.resolve(location, path) : request.headers.referer; 19 | 20 | return { 21 | status: status, 22 | headers: { 23 | "location": location, 24 | "content-type": "text/plain" 25 | }, 26 | body: ['Go to ' + location + ""] 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /lib/jack/middleware.js: -------------------------------------------------------------------------------- 1 | exports.Cascade = require("./cascade.js").Cascade; 2 | exports.CommonLogger = require("./commonlogger").CommonLogger; 3 | exports.ContentLength = require("./contentlength").ContentLength; 4 | exports.ContentType = require("./contentlength").ContentType; 5 | exports.Directory = require("./dir").Directory; 6 | exports.File = require("./file").File; 7 | exports.Head = require("./head").Head; 8 | exports.JSONP = require("./jsonp").JSONP; 9 | exports.Lint = require("./lint").Lint; 10 | exports.MethodOverride = require("./methodoverride").MethodOverride; 11 | exports.Reloader = require("./reloader").Reloader; 12 | exports.ShowExceptions = require("./showexceptions").ShowExceptions; 13 | exports.ShowStatus = require("./showstatus").ShowStatus; 14 | exports.Static = require("./static").Static; 15 | exports.URLMap = require("./urlmap").URLMap; 16 | -------------------------------------------------------------------------------- /lib/jack/cascade.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cascade tries an request on several apps, and returns the first response 3 | * that is not 404. 4 | */ 5 | var defer = require("promise").defer, 6 | when = require("promise").when; 7 | var Cascade = exports.Cascade = function(apps, status) { 8 | status = status || 404; 9 | 10 | return function(request) { 11 | var i = 0; 12 | var deferred = defer(), 13 | lastResponse; 14 | function next(){ 15 | if(i < apps.length){ 16 | when(apps[i](request), function(response){ 17 | i++; 18 | if (response.status !== status) { 19 | deferred.resolve(response); 20 | }else{ 21 | lastResponse = response; 22 | next(); 23 | } 24 | }, deferred.reject); 25 | }else{ 26 | deferred.resolve(lastResponse); 27 | } 28 | } 29 | next(); 30 | return deferred.promise; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/jack/showexceptions.js: -------------------------------------------------------------------------------- 1 | var when = require("promise").when; 2 | 3 | var ShowExceptions = exports.ShowExceptions = function(nextApp) { 4 | return function(request) { 5 | return when(nextApp(request), 6 | function(response) { 7 | return response; 8 | }, 9 | function(e) { 10 | var backtrace = "
" + e.name + ": " + e.message;
11 |                 if (e.rhinoException) {
12 |                     //FIXME abstract and move to engines/rhino
13 |                     backtrace += "\n" + e.rhinoException.getScriptStackTrace();
14 |                 }
15 |                 // FIXME add a branch for node: e.stack?
16 |                 backtrace += "";
17 |                 return {
18 |                     status: 500,
19 |                     headers: {"content-type":"text/html", "content-length": backtrace.length + ""},
20 |                     body: [backtrace]
21 |                 };
22 |             }
23 |         );
24 |     }
25 | }


--------------------------------------------------------------------------------
/lib/jack/handler/v8cgi.js:
--------------------------------------------------------------------------------
 1 | exports.run = function(app, request, response) {
 2 |     var env = {};
 3 |     
 4 |     // copy CGI variables
 5 |     for (var key in request._headers)
 6 |         env[key] = request._headers[key];
 7 | 
 8 |     env["HTTP_VERSION"]         = env["SERVER_PROTOCOL"]; // legacy
 9 |     
10 |     env["jsgi.version"]         = [0,2];
11 |     env["jsgi.input"]           = null; // FIXME
12 |     env["jsgi.errors"]          = system.stderr;
13 |     env["jsgi.multithread"]     = false;
14 |     env["jsgi.multiprocess"]    = true;
15 |     env["jsgi.run_once"]        = true;
16 |     env["jsgi.url_scheme"]      = "http"; // FIXME
17 |     
18 |     // call the app
19 |     var res = app(env);
20 |     
21 |     // set the status
22 |     response.status(res.status);
23 |     
24 |     // set the headers
25 |     response.header(res.headers);
26 |     
27 |     // output the body
28 |     res.body.forEach(function(bytes) {
29 |         response.write(bytes.toByteString("UTF-8").decodeToString("UTF-8"));
30 |     });
31 | }
32 | 


--------------------------------------------------------------------------------
/lib/jack/auth/digest/params.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright Neville Burnell
 3 |  * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license
 4 |  *
 5 |  * Acknowledgements:
 6 |  * Inspired by Rack::Auth
 7 |  * http://github.com/rack/rack
 8 |  */
 9 | 
10 | var Hash = require("hash").Hash,
11 |     Util = require("util");
12 | 
13 | var UNQUOTED = ['qop', 'nc', 'stale'];
14 | 
15 | var dequote = function(str) {
16 |     var m = str.match(/^["'](.*)['"]$/);
17 |     return  m ? m.pop() : str;
18 | }
19 | 
20 | var extractPairs = function(str) {
21 |     return str.match(/(\w+\=(?:"[^\"]+"|[^,]+))/g) || [];
22 | }
23 | 
24 | var parse = exports.parse = function(h, str) {
25 |     extractPairs(str).forEach(function(pair) {
26 |         var kv = pair.match(/(\w+)=(.*)/);
27 |         h[kv[1]] = dequote(kv[2]);
28 |     });
29 | 
30 |     return h;
31 | };
32 | 
33 | var toString = exports.toString = function(h) {
34 |     return Hash.map(h, function(k, v) {
35 |         return String.concat(k, "=", UNQUOTED.indexOf(k) != -1 ? v.toString() : Util.enquote(v.toString()));
36 |     }).join(', ');
37 | };


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "author": "Tom Robinson (http://tlrobinson.net/)",
 3 |     "contributors": [
 4 |         "Tom Robinson (http://tlrobinson.net/) ",
 5 |         "George Moschovitis (http://blog.gmosx.com/)",
 6 |         "Kris Kowal (http://askawizard.blogspot.com/) ",
 7 |         "Neville Burnell",
 8 |         "Isaac Z. Schlueter (http://blog.izs.me/)",
 9 |         "Jan Varwig",
10 |         "Irakli Gozalishvili (http://rfobic.wordpress.com/)",
11 |         "Kris Zyp (http://www.sitepen.com/blog/author/kzyp/)",
12 |         "Kevin Dangoor (http://www.blueskyonmars.com/)",
13 |         "Antti Holvikari",
14 |         "Tim Schaub"
15 |     ],
16 |     "name": "jack",
17 |     "dependencies": [
18 |         "narwhal"
19 |     ],
20 |     "jars": [
21 |         "jars/simple-4.1.10.jar"
22 |     ],
23 |     "lean": {
24 |         "include": [
25 |             "lib/**/*",
26 |             "package.json"
27 |         ]
28 |     },
29 | 	"overlay": {
30 | 		"node": {
31 | 			"mappings": {
32 | 				"uri": "url"
33 | 			}
34 | 		}
35 | 	},	
36 |     "version": "0.3.0"
37 | }
38 | 


--------------------------------------------------------------------------------
/lib/jack/contenttype.js:
--------------------------------------------------------------------------------
 1 | var STATUS_WITH_NO_ENTITY_BODY = require("jack/utils").STATUS_WITH_NO_ENTITY_BODY,
 2 |     MIME_TYPES = require("jack/mime").MIME_TYPES,
 3 |     DEFAULT_TYPE = "text/plain",
 4 |     when = require("promise").when;
 5 | 
 6 | /**
 7 | * This middleware makes sure that the Content-Type header is set for responses
 8 | * that require it.
 9 | */
10 | exports.ContentType = function(app, options) {
11 |     options = options || {};
12 |     options.MIME_TYPES = options.MIME_TYPES || {};
13 |     
14 |     return function(request) {
15 |         return when(app(request), function(response) {
16 |             if (!STATUS_WITH_NO_ENTITY_BODY(response.status) && response.headers["content-type"]) {
17 |                 var contentType = options.contentType;
18 |                 if (!contentType) {
19 |                     var extension = request.pathInfo.match(/(\.[^.]+|)$/)[0];
20 |                     contentType = options.MIME_TYPES[extension] || MIME_TYPES[extension] || DEFAULT_TYPE;
21 |                 }
22 |                 response.headers["content-type"] = contentType;
23 |             }
24 |             return response;
25 |         });
26 |     }
27 | }
28 | 


--------------------------------------------------------------------------------
/examples/upload.js:
--------------------------------------------------------------------------------
 1 | var Request = require("jack/request").Request,
 2 |     Response = require("jack/response").Response;
 3 | 
 4 | exports.app = function(request) {
 5 |     var wrappedRequest = new Request(request),
 6 |         res = new Response();
 7 |     
 8 |     if (wrappedRequest.isPost())
 9 |     {
10 |         var params = wrappedRequest.params();
11 |         for (var i in params)
12 |         {
13 |             res.setHeader("content-type", "text/plain");
14 |             if (typeof params[i] === "string")
15 |                 res.write(i + " => " + params[i] + "\n");
16 |             else
17 |             {
18 |                 for (var j in params[i])
19 |                     res.write("> " + j + " => " + params[i][j] + "\n");
20 |             }
21 |         }
22 |     }
23 |     else
24 |     {
25 |         res.write('
') 26 | res.write(''); 27 | res.write(''); 28 | res.write(''); 29 | res.write('
'); 30 | } 31 | 32 | return res.finish(); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /lib/jack/contentlength.js: -------------------------------------------------------------------------------- 1 | var utils = require("./utils"), 2 | when = require("jack/promise").when; 3 | 4 | // sets the content-length header on responses with fixed-length bodies 5 | exports.ContentLength = function(app) { 6 | return function(request) { 7 | return when(app(request), function(response) { 8 | if (!utils.STATUS_WITH_NO_ENTITY_BODY(response.status) && 9 | !response.headers["content-length"] && 10 | !(response.headers["transfer-encoding"] && response.headers["transfer-encoding"] !== "identity") && 11 | typeof response.body.forEach === "function") 12 | { 13 | var newBody = [], 14 | length = 0; 15 | 16 | response.body.forEach(function(chunk) { 17 | var binary = chunk.toByteString(); 18 | length += binary.length; 19 | newBody.push(binary); 20 | }); 21 | 22 | response.body = newBody; 23 | response.headers["content-length"] = length + ""; 24 | } 25 | return response; 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/jack/auth/digest/request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | var update = require("hash").Hash.update, 11 | AbstractRequest = require('jack/auth/abstract/request').Request, 12 | DigestParams = require('jack/auth/digest/params'), 13 | DigestNonce = require('jack/auth/digest/nonce'); 14 | 15 | var Request = exports.Request = function(env) { 16 | AbstractRequest.call(this, env); 17 | 18 | this.method = this.env['REQUEST_METHOD']; 19 | } 20 | 21 | Request.prototype = update(Object.create(AbstractRequest.prototype), { 22 | 23 | isDigest: function() { 24 | return this.scheme.search(/^digest$/i) != -1; 25 | }, 26 | 27 | isCorrectUri: function() { 28 | return (this.env['SCRIPT_NAME'] + this.env['PATH_INFO'] == this.uri); 29 | }, 30 | 31 | decodeNonce: function() { 32 | return this._decodedNonce || (this._decodedNonce = DigestNonce.decode(this.nonce)); 33 | }, 34 | 35 | decodeCredentials: function (str) { 36 | DigestParams.parse(this, str); 37 | } 38 | 39 | }); -------------------------------------------------------------------------------- /lib/jack/auth/abstract/request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | HashP = require("hashp").HashP; 11 | 12 | var AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION']; 13 | 14 | var Request = exports.Request = function(env) { 15 | this.env = env; 16 | 17 | if (!this.authorizationKey()) return; 18 | 19 | var parts = HashP.get(env, this._authorizationKey).match(/(\w+) (.*)/); 20 | 21 | this.scheme = parts[1]; 22 | this.decodeCredentials(parts.pop()); 23 | }; 24 | 25 | Request.prototype = { 26 | 27 | authorizationKey: function() { 28 | if (this._authorizationKey) return this._authorizationKey; 29 | 30 | for (var i=0; i < AUTHORIZATION_KEYS.length; i++) { 31 | var key = AUTHORIZATION_KEYS[i]; 32 | if (HashP.includes(this.env, key)) return this._authorizationKey = key; 33 | } 34 | }, 35 | 36 | decodeCredentials: function() { 37 | throw "jack.auth.abstract.request.decodeCredentials(): override required!"; 38 | } 39 | }; -------------------------------------------------------------------------------- /lib/jack/methodoverride.js: -------------------------------------------------------------------------------- 1 | var Request = require("./request").Request; 2 | 3 | /** 4 | * Provides Rails-style HTTP method overriding via the _method parameter or X-HTTP-METHOD-OVERRIDE header 5 | * http://code.google.com/apis/gdata/docs/2.0/basics.html#UpdatingEntry 6 | */ 7 | exports.MethodOverride = function(nextApp) { 8 | return function(request) { 9 | if ((request.method == "POST") && (!request.headers["content-type"].match(/^multipart\/form-data/))) { 10 | var req = new Request(request), 11 | method = request.headers[HTTP_METHOD_OVERRIDE_HEADER] || req.POST(METHOD_OVERRIDE_PARAM_KEY); 12 | if (method && HTTP_METHODS[method.toUpperCase()] === true) { 13 | request.env.jack.methodovverride.original_method = request.method; 14 | request.method = method.toUpperCase(); 15 | } 16 | } 17 | return nextApp(request); 18 | } 19 | } 20 | 21 | var HTTP_METHODS = exports.HTTP_METHODS = {"GET":true, "HEAD":true, "PUT":true, "POST":true, "DELETE":true, "OPTIONS":true}; 22 | var METHOD_OVERRIDE_PARAM_KEY = exports.METHOD_OVERRIDE_PARAM_KEY = "_method"; 23 | var HTTP_METHOD_OVERRIDE_HEADER = exports.HTTP_METHOD_OVERRIDE_HEADER = "x-http-method-override"; 24 | -------------------------------------------------------------------------------- /bin/fcgi-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PORT=8080 4 | 5 | if [ ! "$NARWHAL_PLATFORM" ]; then 6 | export NARWHAL_PLATFORM="k7" 7 | fi 8 | 9 | BIN="" 10 | case "$1" in 11 | /*) # absolute path 12 | BIN="$1" 13 | ;; 14 | *) 15 | BIN=$(which $1) 16 | ;; 17 | esac 18 | 19 | if [ ! "$BIN" ]; then 20 | BIN="$PWD/$1" 21 | fi 22 | shift 23 | 24 | ARGS="" 25 | while [ $# -gt 0 ] 26 | do 27 | ARG=$1 28 | shift 29 | case $ARG in 30 | /*) # absolute path 31 | ARGS="$ARGS $ARG" 32 | ;; 33 | *) 34 | if [ -f "$ARG" ]; then 35 | # convert file paths to absolute 36 | ARGS="$ARGS $PWD/$ARG" 37 | else 38 | # everything else 39 | ARGS="$ARGS $ARG" 40 | fi 41 | ;; 42 | esac 43 | done 44 | 45 | BIN_PATH="$BIN $ARGS" 46 | echo $BIN_PATH 47 | 48 | TMP_CONFIG="/tmp/lighttpd-fcgi.conf" 49 | cat > "$TMP_CONFIG" < (( 61 | "bin-path" => "$BIN_PATH", 62 | "min-procs" => 1, 63 | "max-procs" => 1, 64 | "socket" => "/tmp/lighttpd-fastcgi.sock", 65 | "check-local" => "disable" 66 | )) 67 | ) 68 | EOF 69 | 70 | lighttpd -D -f "$TMP_CONFIG" 71 | -------------------------------------------------------------------------------- /lib/jack/path.js: -------------------------------------------------------------------------------- 1 | 2 | var util = require("util"), // Narwhal Util 3 | utils = require("./utils"); // Jack Utils 4 | 5 | exports.Path = function (paths, notFound) { 6 | if (!paths) 7 | paths = {}; 8 | if (!notFound) 9 | notFound = exports.notFound; 10 | return function (env) { 11 | var path = env.PATH_INFO.substring(1); 12 | var parts = path.split("/"); 13 | var part = env.PATH_INFO.charAt(0) + parts.shift(); 14 | if (util.has(paths, part)) { 15 | env.SCRIPT_NAME = env.SCRIPT_NAME + part; 16 | env.PATH_INFO = env.PATH_INFO.substring(part.length); 17 | return paths[part](env); 18 | } 19 | return notFound(env); 20 | }; 21 | }; 22 | 23 | exports.notFound = function (env) { 24 | return utils.responseForStatus(404, env.PATH_INFO); 25 | }; 26 | 27 | 28 | if (require.main == module.id) { 29 | var jack = require("jack"); 30 | var app = exports.Path({ 31 | "/a": exports.Path({ 32 | "": require("./redirect").Redirect("a/"), 33 | "/": function () { 34 | return { 35 | status : 200, 36 | headers : {"Content-type": "text/plain"}, 37 | body : ["Hello, World!"] 38 | }; 39 | } 40 | }) 41 | }); 42 | exports.app = jack.ContentLength(app); 43 | require("jackup").main(["jackup", module.path]); 44 | } 45 | 46 | -------------------------------------------------------------------------------- /docs/jack-component-status.md: -------------------------------------------------------------------------------- 1 | 2 | Jack Component Status 3 | ===================== 4 | 5 | The following components essentially match those in Rack. 6 | 7 | ### Handlers 8 | 9 | * Servlet: complete, for use with Jetty on Rhino, or [other servlet container](http://github.com/tlrobinson/jack-servlet/) such as Google AppEngine for Java. 10 | * Jetty: complete, simple wrapper for Jetty using Servlet handler ([http://www.mortbay.org/jetty/](http://www.mortbay.org/jetty/)) 11 | * Simple: complete, for use with the Simple webserver ([http://www.simpleframework.org/](http://www.simpleframework.org/)) 12 | * K7: incomplete, for use with the k7 project ([http://github.com/sebastien/k7/](http://github.com/sebastien/k7/)) 13 | * V8CGI: incomplete, for use with the v8cgi project ([http://code.google.com/p/v8cgi/](http://code.google.com/p/v8cgi/)) 14 | 15 | ### Middleware 16 | 17 | * Auth: missing 18 | * Cascade: complete 19 | * CommonLogger: complete 20 | * ContentLength: complete 21 | * Deflater: missing 22 | * Directory: missing 23 | * File: complete 24 | * Head: complete 25 | * JSONP: complete 26 | * Lint: mostly complete (needs stream wrappers) 27 | * MethodOverride: complete 28 | * Mock: missing 29 | * Recursive: missing 30 | * ShowExceptions: simple version complete, needs better HTML output 31 | * ShowStatus: missing 32 | * Static: complete 33 | * URLMap: complete 34 | 35 | ### Utilities 36 | 37 | * jackup: complete 38 | * Request: mostly complete 39 | * Response: mostly complete 40 | -------------------------------------------------------------------------------- /lib/jack/http-params.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows the placement of HTTP headers and method in query parameters. This 3 | * is very useful for situations where it is impossible for the requesting code to set 4 | * headers (such as setting cross-site script tag/JSONP, form posts, and 5 | * setting window.location to download data). 6 | */ 7 | var httpParamRegex = /^http[_-]/; 8 | exports.HttpParams = function(nextApp){ 9 | return function(request){ 10 | var parts = request.queryString.split("&"); 11 | 12 | for(var i = 0; i < parts.length;){ 13 | var nameValue = parts[i].split("="); 14 | if(httpParamRegex.test(nameValue[0])){ 15 | var headerName = nameValue[0].substring(5).toLowerCase(); 16 | if(headerName === "method"){ 17 | request.method = nameValue[1]; 18 | } 19 | else if(headerName === "content"){ 20 | request.body = [decodeURIComponent(nameValue[1])]; 21 | } 22 | else{ 23 | request.headers[headerName] = decodeURIComponent(nameValue[1]); 24 | } 25 | parts.splice(i,1); 26 | }else{ 27 | i++; 28 | } 29 | } 30 | if(parts){ 31 | request.queryString = parts.join("&"); 32 | } 33 | return nextApp(request); 34 | } 35 | } 36 | 37 | // TODO integrate with methodoverride.js 38 | -------------------------------------------------------------------------------- /tests/jack/auth/auth-abstract-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | var assert = require("test/assert"), 11 | base64 = require("base64"), 12 | MockRequest = require("jack/mock").MockRequest, 13 | AbstractHandler = require("jack/auth/abstract/handler").Handler, 14 | AbstractRequest = require("jack/auth/abstract/request").Request; 15 | 16 | /* 17 | * tests for AbstractHandler 18 | */ 19 | 20 | exports.testUnauthorizedDefaultChallenge = function() { 21 | var testRealm = "testRealm"; 22 | var handler = new AbstractHandler({realm:testRealm}); 23 | 24 | handler.issueChallenge = function() { 25 | return ('Basic realm=' + this.realm); 26 | }; 27 | 28 | var resp = handler.Unauthorized(); 29 | 30 | assert.eq(401, resp.status); 31 | assert.eq('text/plain', resp.headers['Content-Type']); 32 | assert.eq('Basic realm='+testRealm, resp.headers['WWW-Authenticate']); 33 | 34 | }; 35 | 36 | exports.testUnauthorizedCustomChallenge = function() { 37 | var testRealm = "testRealm"; 38 | var handler = new AbstractHandler({realm:testRealm}); 39 | 40 | var realm = "Custom realm="+testRealm; 41 | var resp = handler.Unauthorized(realm); 42 | 43 | assert.eq(401, resp.status); 44 | assert.eq('text/plain', resp.headers['Content-Type']); 45 | assert.eq(realm, resp.headers['WWW-Authenticate']); 46 | }; -------------------------------------------------------------------------------- /examples/statuscodedrinkinggame.js: -------------------------------------------------------------------------------- 1 | var HTTP_STATUS_CODES = require("jack/utils").HTTP_STATUS_CODES; 2 | // patch code map for the Hyper Text Coffee Pot Control Protocol 3 | HTTP_STATUS_CODES[418] = "I'm a teapot"; 4 | 5 | var map = exports.map = { 6 | 200: "Calm the fuck down. No one drinks.", 7 | 201: "Create a drinking rule. Then drink.", 8 | 202: "You will drink, after the next persons turn.", 9 | 300: "Choose multiple people to drink.", 10 | 301: "Choose someone to drink with. It's then their turn.", 11 | 305: "Person to your right feeds you a drink.", 12 | 307: "Choose someone to drink.", 13 | 401: "Everyone but you drinks.", 14 | 403: "Miss a turn. Must drink double on next turn.", 15 | 404: "Last person to make a greeting must drink.", 16 | 406: "Must drink twice, loser.", 17 | 409: "Drink, then go again.", 18 | 410: "Remove a drinking rule (if one has been created).", 19 | 411: "Take a looooong drink.", 20 | 412: "You may add a precondition to drinking.", 21 | 413: "Thats what she said! Everyone drinks.", 22 | 416: "Person on your left and right drink with you.", 23 | 417: "Drink before your turn? If not, drink and go again.", 24 | 418: "Sing \"I'm a little teapot\". Drink.", 25 | 500: "Oh fuck, Everone drinks!" 26 | } 27 | 28 | exports.app = function(request) { 29 | var codes = Object.keys(map), 30 | code = codes[Math.floor(codes.length * Math.random())]; 31 | return { 32 | status: code, 33 | headers: {"content-type" : "text/html"}, 34 | body: [code + " ", HTTP_STATUS_CODES[code], "
", map[code]] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/jack/handler/jetty.js: -------------------------------------------------------------------------------- 1 | var Servlet = require("./servlet").Servlet; 2 | 3 | exports.run = function(app, options) { 4 | var options = options || {}; 5 | 6 | var servletHandler = new Servlet(options); 7 | 8 | // need to use JavaAdapter form when using module scoping for some reason 9 | var handler = new JavaAdapter(Packages.org.mortbay.jetty.handler.AbstractHandler, { 10 | handle : function(target, request, response, dispatch){ 11 | try { 12 | servletHandler.process(request, response); 13 | 14 | request.setHandled(true); 15 | } catch(e) { 16 | print("EXCEPTION: " + e); 17 | print(" Name: " + e.name); 18 | print(" Message: " + e.message); 19 | print(" File: " + e.fileName); 20 | print(" Line: " + e.lineNumber); 21 | if (e.javaException) 22 | { 23 | print(" Java Exception: " + e.javaException); 24 | e.javaException.printStackTrace(); 25 | } 26 | if (e.rhinoException) 27 | { 28 | print(" Rhino Exception: " + e.rhinoException); 29 | e.rhinoException.printStackTrace(); 30 | } 31 | throw e; 32 | } 33 | } 34 | }); 35 | 36 | var port = options["port"] || 8080, 37 | server = new Packages.org.mortbay.jetty.Server(port); 38 | 39 | print("Jack is starting up using Jetty on port " + port); 40 | 41 | server.setHandler(handler); 42 | server.start(); 43 | } 44 | -------------------------------------------------------------------------------- /lib/jack/auth/digest/nonce.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | var base64 = require("base64"), 11 | trim = require("util").trim, 12 | update = require("hash").Hash.update, 13 | Handler = require('jack/auth/digest/handler'); 14 | 15 | // params include 16 | // 17 | // digest, optional: 18 | // timestamp, optional: 19 | // privateKey needs to set to a constant string 20 | // timeLimit, optional integer (number of milliseconds) to limit the validity of the generated nonces. 21 | 22 | var decode = exports.decode = function(str) { 23 | var parts = base64.decode(str).match(/(\d+) (.*)/); 24 | 25 | return new Nonce({ 26 | timestamp: parseInt(parts[1]), 27 | digest: parts.pop() 28 | }); 29 | }; 30 | 31 | 32 | var Nonce = exports.Nonce = function(params) { 33 | if (params) update(this, params); 34 | 35 | //defaults 36 | if (!this.timestamp) this.timestamp = new Date().getTime(); //milliseconds since 1970 37 | } 38 | 39 | Nonce.prototype = { 40 | 41 | isValid: function() { 42 | return this.digest == this.toDigest(); 43 | }, 44 | 45 | toString: function() { 46 | return trim(base64.encode([this.timestamp, this.toDigest()].join(' '))); 47 | }, 48 | 49 | toDigest: function() { 50 | return Handler.base16md5([this.timestamp, this.privateKey].join(':')); 51 | }, 52 | 53 | isFresh: function() { 54 | if (!this.timeLimit) return true; // no time limit 55 | return (new Date().getTime() - this.timestamp < this.timeLimit); 56 | } 57 | }; -------------------------------------------------------------------------------- /tests/jack/session-cookie-tests.js: -------------------------------------------------------------------------------- 1 | var assert = require("test/assert"), 2 | Jack = require("jack"), 3 | Request = require("jack/request").Request, 4 | Response = require("jack/response").Response, 5 | MockRequest = require("jack/mock").MockRequest, 6 | Cookie = require("jack/session/cookie").Cookie; 7 | 8 | var helloApp = function(block) { 9 | return Jack.URLMap({ 10 | "/": function(env) { 11 | block(env); 12 | return { 13 | status: 200, 14 | headers: { 'Content-Type': 'text/html'}, 15 | body: ["hello"] 16 | }; 17 | } 18 | }); 19 | }; 20 | 21 | exports.testCreatesNewCookie = function(){ 22 | var env = MockRequest.envFor(null, "", {}); 23 | 24 | var setMyKey = function(env){ 25 | env["jsgi.session"]["mykey"] = "myval"; 26 | }; 27 | 28 | var app = helloApp(setMyKey); 29 | 30 | var response = new MockRequest(new Cookie(app, { secret: "secret" })).GET("/"); 31 | 32 | assert.isTrue(response.headers["Set-Cookie"] != undefined, "Should have defined 'Set-Cookie'"); 33 | assert.isTrue(response.headers["Set-Cookie"].match(/jsgi.session=/g) != null, "Should have created a new cookie"); 34 | } 35 | 36 | exports.testRetrieveSessionValue = function(){ 37 | var retrievedVal = null; 38 | 39 | var retrieveMyKey = function(env) { 40 | retrievedVal = env["jsgi.session"]["mykey"]; 41 | }; 42 | 43 | var app = helloApp(retrieveMyKey); 44 | 45 | new MockRequest(new Cookie(app, {secret: "secret" })).GET("/", { "HTTP_COOKIE": "jsgi.session=%7B%22mykey%22%3A%22myval%22%7D--2m6GuCKsHcPfqaI2Yezhy7kdo%2Fg%3D" }); 46 | 47 | assert.isEqual("myval", retrievedVal); 48 | } 49 | 50 | -------------------------------------------------------------------------------- /lib/jack/handler/cgi.js: -------------------------------------------------------------------------------- 1 | var HTTP_STATUS_CODES = require("jack/utils").HTTP_STATUS_CODES; 2 | 3 | var lineEnding = "\n"; 4 | 5 | exports.run = function(app) { 6 | var input = system.stdin.raw; 7 | var output = system.stdout.raw; 8 | var error = system.stderr; 9 | 10 | var env = {}; 11 | 12 | // copy CGI variables 13 | for (var key in system.env) 14 | env[key] = system.env[key]; 15 | 16 | env["jsgi.version"] = [0,2]; 17 | env["jsgi.input"] = input; 18 | env["jsgi.errors"] = error; 19 | env["jsgi.multithread"] = false; 20 | env["jsgi.multiprocess"] = true; 21 | env["jsgi.run_once"] = true; 22 | env["jsgi.url_scheme"] = "http"; 23 | 24 | // call the app 25 | var result; 26 | try { 27 | result = app(env); 28 | } catch (e) { 29 | result = { 30 | status : 500, 31 | headers : {}, 32 | body : ["Internal Server Error"] 33 | }; 34 | } 35 | 36 | // status line 37 | result.headers["Status"] = result.status + " " + (HTTP_STATUS_CODES[result.status] || "Unknown"); 38 | 39 | // headers 40 | for (var name in result.headers) { 41 | var values = result.headers[name].split(/\n/g); 42 | values.forEach(function(value) { 43 | output.write((name + ": " + value + lineEnding).toByteString("UTF-8")); 44 | }); 45 | } 46 | 47 | // end headers 48 | output.write(lineEnding.toByteString("UTF-8")); 49 | output.flush(); 50 | 51 | // body 52 | result.body.forEach(function(bytes) { 53 | output.write(bytes.toByteString("UTF-8")); 54 | output.flush(); 55 | }); 56 | 57 | output.flush(); 58 | output.close(); 59 | } 60 | -------------------------------------------------------------------------------- /lib/jack/jsonp.js: -------------------------------------------------------------------------------- 1 | var Request = require("./request").Request, 2 | when = require("promise").when; 3 | 4 | // Wraps a response in a JavaScript callback if provided in the "callback" parameter, 5 | // JSONP style, to enable cross-site fetching of data. Be careful where you use this. 6 | // http://bob.pythonmac.org/archives/2005/12/05/remote-json-jsonp/ 7 | var JSONP = exports.JSONP = function(app, callbackParameter) { 8 | return function(request) { 9 | return when(app(request), function(response) { 10 | var req = new Request(request); 11 | var callback = req.params(callbackParameter || "callback"); 12 | 13 | if (callback) { 14 | var header = (callback + "(").toByteString(), 15 | footer = (")").toByteString(); 16 | 17 | response.headers["content-type"] = "application/javascript"; 18 | 19 | // Assume the Content-Length was correct before and simply add the length of the padding. 20 | if (response.headers["content-length"]) { 21 | var contentLength = parseInt(response.headers["content-length"], 10); 22 | contentLength += header.length + footer.length; 23 | response.headers["content-length"] = contentLength + ""; 24 | } 25 | 26 | var body = response.body; 27 | response.body = { 28 | forEach : function(chunk) { 29 | chunk(header); 30 | body.forEach(chunk); 31 | chunk(footer); 32 | } 33 | } 34 | } 35 | 36 | return response; 37 | }); 38 | } 39 | } -------------------------------------------------------------------------------- /examples/async.js: -------------------------------------------------------------------------------- 1 | var Request = require("jack/request").Request, 2 | Response = require("jack/response").Response, 3 | AsyncResponse = require("jack/response").AsyncResponse; 4 | 5 | var sessions = []; 6 | 7 | var map = {}; 8 | 9 | map["/"] = function(request) { 10 | var res = new Response(); 11 | 12 | res.write(''); 13 | res.write(''); 14 | res.write(''); 15 | res.write('listen'); 16 | res.write(''); 17 | 18 | return res.finish(); 19 | } 20 | 21 | map["/send"] = function(request) { 22 | var res = new Response(), 23 | message = new Request(request).params("message"); 24 | 25 | var total = sessions.length; 26 | sessions = sessions.filter(function(session) { 27 | try { 28 | session.write("received: " + message + "
"); 29 | } catch (e) { 30 | return false; 31 | } 32 | return true; 33 | }); 34 | 35 | res.write("sent '" + message + "' to " + sessions.length + " clients, " + (total - sessions.length) + " closed."); 36 | 37 | return res.finish(); 38 | } 39 | 40 | map["/listen"] = function(env) { 41 | var response = new AsyncResponse({ 42 | status : 200, 43 | headers : {"transfer-encoding" : "chunked"}, 44 | body : [Array(1024).join(" ")] 45 | }); 46 | 47 | sessions.push(response); 48 | 49 | return response; 50 | } 51 | 52 | // apply the URLMap 53 | exports.app = require("jack/urlmap").URLMap(map); 54 | -------------------------------------------------------------------------------- /docs/writing-jsgi-middleware.md: -------------------------------------------------------------------------------- 1 | 2 | Writing Jack Middleware 3 | ======================= 4 | 5 | Jack middleware performs pre or post processing on requests and responses, such as logging, authentication, etc. Most Jack middleware, by convention, is a function that takes in one argument, "app" (a Jack application, possibly wrapped in other middleware) and returns another Jack application (i.e. another function that takes in an "env" argument and returns a three element array). The returned Jack application will typically optionally do some preprocessing on the request, followed by calling the "app" that was provided, optionally followed by some post processing. 6 | 7 | For example, the "Head" middleware calls the original "app", then checks to see if the request HTTP method was "HEAD". If so, it clears the body of response before returning it, since HEAD requests shouldn't have a response body: 8 | 9 | function Head(app) { 10 | return function(env) { 11 | var result = app(env); 12 | if (env["REQUEST_METHOD"] === "HEAD") 13 | result.body = []; 14 | return result; 15 | } 16 | } 17 | 18 | This style of middleware makes use of a closure to store a reference to the original app. 19 | 20 | A more complicated middleware might need to perform post-processing on the body contents. A common pattern is to call the app, then store the body as a property of a "context" and return the context itself in place of the body. The context defines a "forEach" method on the context, which proxies to the stored body property. 21 | 22 | It is important to proxy the response body rather than buffer the entire response when dealing with streaming applications, otherwise the middleware will prevent the app from streaming. A good example of this pattern is the CommonLogger middleware, which does this in order to calculate the body length for logging. 23 | -------------------------------------------------------------------------------- /docs/getting-started-with-jack.md: -------------------------------------------------------------------------------- 1 | 2 | Getting Started With Jack 3 | ========================= 4 | 5 | Jack currently supports the [Jetty](http://www.mortbay.org/jetty/) (and other servlet containers) and [Simple](http://www.simpleframework.org/) webservers using [Rhino](http://www.mozilla.org/rhino/). It's also easy to add support for other webservers. 6 | 7 | The current Jack implementation uses Narwhal for support. Narwhal is a JavaScript standard library (based on the ServerJS standard: [https://wiki.mozilla.org/ServerJS](https://wiki.mozilla.org/ServerJS)) and is located at [http://narwhaljs.org/](http://narwhaljs.org/) 8 | 9 | To start working with Jack, follow the [Narwhal Quick Start](http://narwhaljs.org/quick-start.html) guide, which includes installing Jack. 10 | 11 | Then run one of the examples (paths relative to Narwhal installation): 12 | 13 | jackup packages/jack/example/example.js 14 | jackup packages/jack/example/comet.js 15 | 16 | Or if the current directory contains "jackconfig.js" you can just run "jackup" 17 | 18 | jackup 19 | 20 | This is equivalent to: 21 | 22 | jackup jackconfig.js 23 | 24 | A Jackup configuration file is a normal Narwhal module that exports a function called "app": 25 | 26 | exports.app = function(env) { 27 | return { 28 | status : 200, 29 | headers : {"Content-Type":"text/plain"}, 30 | body : ["Hello world!"] 31 | }; 32 | } 33 | 34 | If the module also exports a function with the same name as the chosen environment (using the "-E" command line option, "development" by default) that function will be used to apply middleware to your application. This allows you to define custom sets of middleware for different environments. For example: 35 | 36 | exports.development = function(app) { 37 | return Jack.CommonLogger( 38 | Jack.ShowExceptions( 39 | Jack.Lint( 40 | Jack.ContentLength(app)))); 41 | } 42 | 43 | To see other options of Jackup, use the "-h" option: 44 | 45 | jackup -h 46 | -------------------------------------------------------------------------------- /lib/jack/directory.js: -------------------------------------------------------------------------------- 1 | var util = require("util"); 2 | 3 | exports.Directory = function (paths, notFound) { 4 | if (!paths) 5 | paths = {}; 6 | if (!notFound) 7 | notFound = exports.notFound; 8 | return function (request) { 9 | if (!/^\//.test(request.pathInfo)) { 10 | var location = 11 | (request.scheme || 'http') + 12 | '://' + 13 | (request.headers.host || ( 14 | request.host + 15 | (request.port == "80" ? "" : ":" + request.port) 16 | )) + 17 | (request.scriptName || '') + 18 | request.pathInfo + "/"; 19 | return { 20 | status : 301, 21 | headers : { 22 | "location": location, 23 | "content-type": "text/plain" 24 | }, 25 | body : ['Permanent Redirect: ' + location] 26 | }; 27 | } 28 | var path = request.pathInfo.substring(1); 29 | var parts = path.split("/"); 30 | var part = parts.shift(); 31 | if (util.has(paths, part)) { 32 | request.scriptName = request.scriptName + "/" + part; 33 | request.pathInfo = path.substring(part.length); 34 | return paths[part](request); 35 | } 36 | return notFound(request); 37 | }; 38 | }; 39 | 40 | exports.notFound = function (request) { 41 | return utils.responseForStatus(404, request.pathInfo); 42 | }; 43 | 44 | if (require.main == module.id) { 45 | var jack = require("jack"); 46 | var app = exports.Directory({ 47 | "a": exports.Directory({ 48 | "": function () { 49 | return { 50 | status : 200, 51 | headers : {"content-type": "text/plain"}, 52 | body : ["Hello, World!"] 53 | }; 54 | } 55 | }) 56 | }); 57 | exports.app = jack.ContentLength(app); 58 | require("jackup").main(["jackup", module.path]); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /lib/jack/handler/fastcgi-k7.js: -------------------------------------------------------------------------------- 1 | var ByteString = require("binary").ByteString; 2 | 3 | exports.run = function(app) { 4 | var f = new net.http.server.fcgi.FCGI(); 5 | 6 | while (f.accept() >= 0) { 7 | var fcgiEnv = f.getEnv(), 8 | env = {}; 9 | 10 | for (var key in fcgiEnv) { 11 | var newKey = key 12 | if (newKey === "HTTP_CONTENT_LENGTH") 13 | newKey = "CONTENT_LENGTH"; 14 | else if (newKey === "HTTP_CONTENT_TYPE") 15 | newKey = "CONTENT_TYPE"; 16 | env[newKey] = fcgiEnv[key]; 17 | } 18 | 19 | if (env["SCRIPT_NAME"] === "/") { 20 | env["SCRIPT_NAME"] = ""; 21 | env["PATH_INFO"] = "/"; 22 | } 23 | 24 | env["jsgi.version"] = [0,2]; 25 | 26 | var input = f.getRawRequest(), 27 | offset = 0; 28 | env["jsgi.input"] = { 29 | read : function(length) { 30 | var read; 31 | if (typeof length === "number") 32 | read = input.substring(offset, offset+length); 33 | else 34 | read = input.substring(offset); 35 | offset += read.length; 36 | return new ByteString(read); 37 | }, 38 | close : function(){} 39 | } 40 | 41 | env["jsgi.errors"] = system.stderr; 42 | env["jsgi.multithread"] = false; 43 | env["jsgi.multiprocess"] = true; 44 | env["jsgi.run_once"] = true; 45 | env["jsgi.url_scheme"] = "http"; // FIXME 46 | 47 | var res = app(env); 48 | 49 | // set the headers 50 | for (var key in res.headers) { 51 | res.headers[key].split("\n").forEach(function(value) { 52 | response.add(key, value); 53 | }); 54 | } 55 | f.write("\r\n"); 56 | 57 | res.body.forEach(function(bytes) { 58 | f.write(bytes.toByteString("UTF-8").decodeToString("UTF-8")); 59 | }); 60 | } 61 | 62 | f.free(); 63 | } 64 | -------------------------------------------------------------------------------- /lib/jack/handler/fastcgi-rhino-jna.js: -------------------------------------------------------------------------------- 1 | // this uses the Java JNA library to invoke libfcgi from Java. Incomplete. 2 | 3 | exports.run = function(app, options) { 4 | var fcgi = Packages.com.sun.jna.NativeLibrary.getInstance("/opt/local/lib/libfcgi.dylib"), 5 | FCGX_Init = fcgi.getFunction("FCGX_Init"), 6 | FCGI_Accept = fcgi.getFunction("FCGI_Accept"), 7 | FCGI_puts = fcgi.getFunction("FCGI_puts"); 8 | 9 | FCGX_Init.invoke([]); 10 | 11 | while (FCGI_Accept.invokeInt([]) >= 0) { 12 | var env = {}; 13 | 14 | env["CONTENT_LENGTH"] = "0"; 15 | env["CONTENT_TYPE"] = "text/plain"; 16 | 17 | env["SCRIPT_NAME"] = "";//String(request.getServletPath() || ""); 18 | env["PATH_INFO"] = "/";//String(request.getPathInfo() || ""); 19 | 20 | env["REQUEST_METHOD"] = "GET";//String(request.getMethod() || ""); 21 | env["SERVER_NAME"] = "localhost";//String(request.getServerName() || ""); 22 | env["SERVER_PORT"] = "";//String(request.getServerPort() || ""); 23 | env["QUERY_STRING"] = "";//String(request.getQueryString() || ""); 24 | env["SERVER_PROTOCOL"] = "HTTP/1.1";//String(request.getProtocol() || ""); 25 | env["HTTP_VERSION"] = env["SERVER_PROTOCOL"]; // legacy 26 | 27 | env["REMOTE_HOST"] = "127.0.0.1"; 28 | 29 | env["HTTP_HOST"] = "localhost"; 30 | 31 | env["jsgi.version"] = [0,2]; 32 | env["jsgi.input"] = system.stdin; 33 | env["jsgi.errors"] = system.stderr; 34 | env["jsgi.multithread"] = true; 35 | env["jsgi.multiprocess"] = true; 36 | env["jsgi.run_once"] = false; 37 | env["jsgi.url_scheme"] = "http"; 38 | 39 | var response = app(env); 40 | 41 | FCGI_puts.invoke(["Content-Type: text/html\n"]); 42 | 43 | response.body.forEach(function(data) { 44 | FCGI_puts.invoke([data.toByteString().decodeToString()]); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/jack/request-tests.js: -------------------------------------------------------------------------------- 1 | var assert = require("test/assert"), 2 | Request = require("jack/request").Request, 3 | MockRequest = require("jack/mock").MockRequest; 4 | 5 | exports.testParseCookies = function() { 6 | var req = new Request(MockRequest.envFor(null, "", { "HTTP_COOKIE" : "foo=bar;quux=h&m" })); 7 | 8 | assert.isSame({"foo" : "bar", "quux" : "h&m"}, req.cookies()); 9 | assert.isSame({"foo" : "bar", "quux" : "h&m"}, req.cookies()); 10 | delete req.env["HTTP_COOKIE"]; 11 | assert.isSame({}, req.cookies()); 12 | } 13 | 14 | exports.testParseCookiesRFC2109 = function() { 15 | var req = new Request(MockRequest.envFor(null, "", { "HTTP_COOKIE" : "foo=bar;foo=car" })); 16 | 17 | assert.isSame({"foo" : "bar"}, req.cookies()); 18 | } 19 | 20 | exports.testMediaType = function() { 21 | var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "text/html" })); 22 | assert.isEqual(req.mediaType(), "text/html"); 23 | assert.isEqual(req.contentType(), "text/html"); 24 | 25 | var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "text/html; charset=utf-8" })); 26 | assert.isEqual(req.mediaType(), "text/html"); 27 | assert.isEqual(req.contentType(), "text/html; charset=utf-8"); 28 | assert.isEqual(req.mediaTypeParams()["charset"], "utf-8"); 29 | assert.isEqual(req.contentCharset(), "utf-8"); 30 | 31 | var req = new Request(MockRequest.envFor(null, "", {})); 32 | assert.isEqual(req.mediaType(), null); 33 | assert.isEqual(req.contentType(), null); 34 | } 35 | 36 | exports.testHasFormData = function() { 37 | var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "application/x-www-form-urlencoded" })); 38 | assert.isEqual(req.hasFormData(), true); 39 | 40 | var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "multipart/form-data" })); 41 | assert.isEqual(req.hasFormData(), true); 42 | 43 | var req = new Request(MockRequest.envFor(null, "", {})); 44 | assert.isEqual(req.hasFormData(), true); 45 | 46 | var req = new Request(MockRequest.envFor(null, "", { "CONTENT_TYPE" : "text/html" })); 47 | assert.isEqual(req.hasFormData(), false); 48 | } 49 | -------------------------------------------------------------------------------- /docs/writing-jsgi-applications.md: -------------------------------------------------------------------------------- 1 | 2 | Writing JSGI Applications 3 | ========================= 4 | 5 | A JSGI application is simply a JavaScript function. It takes a single environment argument, and it should return an array containing three elements: the status code (an integer), the headers values (a hash), and a body object (anything that responds to the "forEach" method which yields objects that have a "toByteString()" method). 6 | 7 | Narwhal has extended JavaScript String, ByteArray, and ByteString respond to "toByteString" (so they are valid "body" responses), thus the following is a valid JSGI application: 8 | 9 | function(env) { 10 | return { 11 | status : 200, 12 | headers : {"Content-Type":"text/plain"}, 13 | body : ["Hello world!"] 14 | }; 15 | } 16 | 17 | If you need something more complex with extra state, you can provide a "constructor" in the form of a function: 18 | 19 | MyApp = function(something) { 20 | return function(env) { 21 | return { 22 | status : 200, 23 | headers : {"Content-Type":"text/plain"}, 24 | body : ["Hello " + this.something + "!"] 25 | }; 26 | } 27 | } 28 | 29 | app = MyApp("Fred"); 30 | 31 | Be careful to ensure your application and middleware is thread-safe if you plan to use a multithreaded server like Jetty and Simple. 32 | 33 | The first (and only) argument to the application method is the "environment" object, which contains a number of properties. Many of the standard CGI environment variables are included, as well as some JSGI specific properties which are prefixed with "jsgi.". 34 | 35 | The Request and Response objects are part of Jack, not the JSGI specification, but may be helpful in parsing request parameters, and building a valid response. They are used as follows: 36 | 37 | var req = new Jack.Request(env); 38 | var name = req.GET("name"); 39 | 40 | var resp = new Jack.Response(); 41 | resp.setHeader("Content-Type", "text/plain"); 42 | resp.write("hello "); 43 | resp.write(name); 44 | resp.write("!"); 45 | return resp.finish(); 46 | 47 | This is roughly equivalent to returning `{ status : 200, headers : {"Content-Type" : "text/plain"}, body : ["hello "+name+"!"] }` 48 | -------------------------------------------------------------------------------- /lib/jack/urlmap.js: -------------------------------------------------------------------------------- 1 | var utils = require("./utils"); 2 | 3 | 4 | function squeeze(s) { 5 | var set = arguments.length > 0 ? "["+Array.prototype.join.call(arguments.slice(1), '')+"]" : ".|\\n", 6 | regex = new RegExp("("+set+")\\1+", "g"); 7 | 8 | return s.replace(regex, "$1"); 9 | }; 10 | 11 | var URLMap = exports.URLMap = function(map, options) { 12 | var options = options || { longestMatchFirst : true }, 13 | mapping = []; 14 | 15 | for (location in map) { 16 | var app = map[location], 17 | host = null, 18 | match; 19 | 20 | if (match = location.match(/^https?:\/\/(.*?)(\/.*)/)) 21 | { 22 | host = match[1]; 23 | location = match[2]; 24 | } 25 | 26 | if (location.charAt(0) != "/") 27 | throw new Error("paths need to start with / (was: " + location + ")"); 28 | 29 | mapping.push([host, location.replace(/\/+$/,""), app]); 30 | } 31 | // if we want to match longest matches first, then sort 32 | if (options.longestMatchFirst) { 33 | mapping = mapping.sort(function(a, b) { 34 | return (b[1].length - a[1].length) || ((b[0]||"").length - (a[0]||"").length); 35 | }); 36 | } 37 | 38 | return function(request) { 39 | var path = request.pathInfo ? squeeze(request.pathInfo, "/") : "", 40 | hHost = request.headers.host, sName = request.host, sPort = request.port; 41 | 42 | for (var i = 0; i < mapping.length; i++) 43 | { 44 | var host = mapping[i][0], location = mapping[i][1], app = mapping[i][2]; 45 | 46 | if ((host === hHost || host === sName || (host === null && (hHost === sName || hHost === sName + ":" + sPort))) && 47 | (location === path.substring(0, location.length)) && 48 | (path.charAt(location.length) === "" || path.charAt(location.length) === "/")) 49 | { 50 | // FIXME: instead of modifying these, create a copy of "request" 51 | request.scriptName += location; 52 | request.pathInfo = path.substring(location.length); 53 | 54 | return app(request); 55 | } 56 | } 57 | return exports.notFound(request); 58 | } 59 | } 60 | 61 | exports.notFound = function (request) { 62 | return utils.responseForStatus(404, request.pathInfo); 63 | }; 64 | -------------------------------------------------------------------------------- /lib/jack/auth/basic/handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | var base64 = require("base64"), 11 | update = require("hash").Hash.update, 12 | AbstractHandler = require('jack/auth/abstract/handler').Handler, 13 | AbstractRequest = require('jack/auth/abstract/request').Request; 14 | 15 | /******************************************************** 16 | * Request 17 | * inherits from AbstractRequest 18 | ********************************************************/ 19 | 20 | var Request = exports.Request = function(env) { 21 | AbstractRequest.call(this, env); 22 | } 23 | 24 | Request.prototype = update(Object.create(AbstractRequest.prototype), { 25 | 26 | isBasic: function() { 27 | return this.scheme.search(/^basic$/i) != -1; 28 | }, 29 | 30 | decodeCredentials: function (str) { 31 | var decoded = base64.decode(str).match(/(\w+):(.*)/); 32 | this.username = decoded[1]; 33 | this.password = decoded[2]; 34 | } 35 | }); 36 | 37 | /******************************************************** 38 | * Handler 39 | * inherits from AbstractHandler 40 | ********************************************************/ 41 | 42 | var Handler = exports.Handler = function(params) { 43 | AbstractHandler.call(this, params); 44 | } 45 | 46 | Handler.prototype = update(Object.create(AbstractHandler.prototype), { 47 | 48 | // generate() returns a Basic Auth JSGI handler for the app 49 | generate: function(app) { 50 | var self = this; 51 | 52 | return function(env) { 53 | 54 | var request = new Request(env); 55 | 56 | if (!request.authorizationKey()) return self.Unauthorized(); 57 | if (!request.isBasic()) return self.BadRequest; 58 | 59 | //isValid is provided by the middleware user 60 | if (!self.isValid(request)) return self.Unauthorized(); 61 | 62 | env['REMOTE_USER'] = request.username; 63 | return app(env); 64 | } 65 | }, 66 | 67 | issueChallenge: function() { 68 | return ('Basic realm=' + this.realm); 69 | } 70 | }); 71 | 72 | /******************************************************** 73 | * Basic Auth Middleware 74 | ********************************************************/ 75 | 76 | exports.Middleware = function(app, params) { 77 | return new Handler(params).generate(app); 78 | }; -------------------------------------------------------------------------------- /lib/jack/handler/simple.js: -------------------------------------------------------------------------------- 1 | // handler for Simple (http://simpleweb.sourceforge.net/) based on the servlet handler 2 | 3 | var delegator = require("./worker-delegator"), 4 | HTTP_STATUS_CODES = require("../utils").HTTP_STATUS_CODES; 5 | 6 | exports.run = function(app, options) { 7 | options = options || {}; 8 | 9 | // need to use JavaAdapter form when using module scoping for some reason 10 | var handler = new Packages.org.simpleframework.http.core.ContainerServer(new JavaAdapter(Packages.org.simpleframework.http.core.Container, { 11 | handle : function(request, response) { 12 | try { 13 | process(request, response); 14 | } catch (e) { 15 | print("ERROR: " + e + " ["+e.message+"]"); 16 | if (e.rhinoException) 17 | e.rhinoException.printStackTrace(); 18 | else if(e.javaException) 19 | e.javaException.printStackTrace(); 20 | throw e; 21 | } 22 | } 23 | }), 1); // specify that only one thread will be used since the 24 | // request handing is only delegating the requests onto a queue for distribution to workers 25 | 26 | // different version 27 | var port = options.port || 8080, 28 | address = options.host ? new Packages.java.net.InetSocketAddress(options.host, port) : new Packages.java.net.InetSocketAddress(port), 29 | connection; 30 | 31 | if (typeof Packages.org.simpleframework.transport.connect.SocketConnection === "function") 32 | connection = new Packages.org.simpleframework.transport.connect.SocketConnection(handler); 33 | else if (typeof Packages.org.simpleframework.http.connect.SocketConnection === "function") 34 | connection = new Packages.org.simpleframework.http.connect.SocketConnection(handler); 35 | else 36 | throw new Error("Simple SocketConnection not found, missing .jar?"); 37 | 38 | print("Jack is starting up using Simple on port " + port); 39 | 40 | connection.connect(address); 41 | var process = function(request, response) { 42 | // the actual simpleframework request handler, just puts them in the queue 43 | if(!delegator.enqueue(request, response)){ 44 | response.setCode(503); 45 | response.setText("Service Unavailable"); 46 | var stream = response.setOutputStream(); 47 | stream.close(); 48 | } 49 | }; 50 | delegator.createQueue(options); 51 | delegator.createWorkers("simple-worker", options); 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /lib/jack/session/cookie.js: -------------------------------------------------------------------------------- 1 | var util = require("util"), 2 | Request = require("jack/request").Request, 3 | Response = require("jack/response").Response, 4 | sha = require("sha"), 5 | base64 = require("base64"), 6 | HashP = require("hashp").HashP; 7 | 8 | var loadSession = function(env){ 9 | var options = env["jsgi.session.options"], 10 | key = options.key, 11 | secret = options.secret; 12 | 13 | var req = new Request(env); 14 | var cookie = req.cookies()[key]; 15 | 16 | if (cookie){ 17 | var parts = decodeURIComponent(cookie).split("--"), 18 | digest = env["jsgi.session.digest"] = parts[1], 19 | sessionData = parts[0]; 20 | 21 | if (digest == base64.encode(sha.hash(sessionData + secret))) { 22 | return JSON.parse(sessionData); 23 | } 24 | } 25 | 26 | return {}; 27 | } 28 | 29 | var commitSession = function(env, jsgiResponse, key, secret){ 30 | var session = env["jsgi.session"]; 31 | 32 | if (!session) return jsgiResponse; 33 | 34 | var sessionData = JSON.stringify(session); 35 | 36 | var digest = base64.encode(sha.hash(sessionData + secret)); 37 | 38 | // do not serialize if the session is not dirty. 39 | if (digest == env["jsgi.session.digest"]) return jsgiResponse; 40 | 41 | sessionData = sessionData + "--" + digest; 42 | 43 | if (sessionData.length > 4096) { 44 | env["jsgi.errors"] += "Session Cookie data size exceeds 4k! Content dropped"; 45 | return jsgiResponse; 46 | } 47 | 48 | var options = env["jsgi.session.options"]; 49 | 50 | var cookie = { path: "/", value: sessionData }; 51 | if (options["expires_after"]) 52 | cookie.expires = new Date() + options["expires_after"]; 53 | 54 | var response = new Response(jsgiResponse.status, jsgiResponse.headers, jsgiResponse.body); 55 | response.setCookie(key, cookie); 56 | 57 | return response; 58 | } 59 | 60 | /** 61 | * Cookie Session Store middleware. 62 | * Does not implicitly deserialize the session, only serializes the session if 63 | * dirty. 64 | */ 65 | var Cookie = exports.Cookie = function(app, options) { 66 | options = options || {}; 67 | util.update(options, /* default options */ { 68 | key: "jsgi.session", 69 | domain: null, 70 | path: "/", 71 | expire_after: null 72 | }); 73 | 74 | if (!options.secret) throw new Error("Session secret not defined"); 75 | 76 | var key = options.key, 77 | secret = options.secret; 78 | 79 | return function(env) { 80 | env["jsgi.session.loadSession"] = loadSession; 81 | env["jsgi.session.options"] = options; 82 | 83 | var jsgiResponse = app(env); 84 | 85 | return commitSession(env, jsgiResponse, key, secret); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/jack/file.js: -------------------------------------------------------------------------------- 1 | var file = require("file"), 2 | Utils = require("./utils"), 3 | MIME = require("./mime"); 4 | 5 | exports.File = function(root, options) { 6 | root = file.path(root).absolute(); 7 | options = options || {}; 8 | var index = options.index || "index.html"; 9 | 10 | return function(request, pathInfo) { 11 | pathInfo = Utils.unescape(pathInfo); 12 | if (pathInfo.indexOf("..") >= 0) 13 | return Utils.responseForStatus(403); 14 | 15 | var path = root + pathInfo; // don't want to append a "/" if PATH_INFO is empty 16 | //path = resource(path); 17 | try { 18 | if (path !== undefined) { 19 | if ( file.isFile(path) && file.isReadable(path)) { 20 | return send(path); 21 | } 22 | else if ( file.isDirectory(path)) { 23 | path = file.join(path, index); 24 | if ( file.isFile(path) && file.isReadable(path)) { 25 | return send(path); 26 | } 27 | } 28 | } 29 | } catch(e) { 30 | request.jsgi.errors.print("Jack.File error: " + e); 31 | } 32 | 33 | return Utils.responseForStatus(404, pathInfo); 34 | 35 | function send (path) { 36 | // efficiently serve files if the server supports "X-Sendfile" 37 | if (request["HTTP_X_ALLOW_SENDFILE"]) { 38 | return { 39 | status : 200, 40 | headers : { 41 | "X-Sendfile" : path, 42 | "Content-Type" : mime.mimeType(file.extension(path), "text/plain"), 43 | "Content-Length" : "0"//String(file.size(path)) 44 | }, 45 | body : [] 46 | }; 47 | } else { 48 | var contents = file.read(path, { mode : "b" }); 49 | if (contents) 50 | return serve(path, contents); 51 | } 52 | } 53 | } 54 | } 55 | 56 | function serve(path, allowSendfile) { 57 | // TODO: once we have streams that respond to forEach, just return the stream. 58 | // efficiently serve files if the server supports "X-Sendfile" 59 | if (allowSendfile) { 60 | return { 61 | status : 200, 62 | headers : { 63 | "X-Sendfile" : path.toString(), 64 | "Content-Type" : MIME.mimeType(file.extension(path), "text/plain"), 65 | "Content-Length" : "0" 66 | }, 67 | body : [] 68 | }; 69 | } else { 70 | var body = path.read({ mode : "b" }); 71 | return { 72 | status : 200, 73 | headers : { 74 | "Last-Modified" : file.mtime(path).toUTCString(), 75 | "Content-Type" : MIME.mimeType(file.extension(path), "text/plain"), 76 | "Content-Length" : body.length.toString(10) 77 | }, 78 | body : [body] 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/jack/commonlogger.js: -------------------------------------------------------------------------------- 1 | var when = require("promise").when; 2 | 3 | var CommonLogger = exports.CommonLogger = function(nextApp, logger) { 4 | logger = logger || {}; 5 | 6 | return function(request) { 7 | var time = new Date(); 8 | logger.log = logger.log || function log(string) { 9 | request.jsgi.errors.print(string); 10 | request.jsgi.errors.flush(); 11 | }; 12 | return when(nextApp(request), function(response) { 13 | var data = response.body, 14 | length = 0; 15 | 16 | response.body = { 17 | forEach: function(write) { 18 | return when( 19 | data.forEach(function(chunk) { 20 | write(chunk); 21 | length += chunk.toByteString().length; 22 | }), 23 | function() { 24 | var now = new Date(); 25 | 26 | // Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common 27 | // lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 - 28 | // %{%s - %s [%s] "%s %s%s %s" %d %s\n} % 29 | 30 | var address = request.headers['x-forwarded-for'] || request.remoteAddr || "-", 31 | user = request.remoteUser || "-", 32 | timestamp = CommonLogger.formatDate(now), 33 | method = request.method, 34 | path = (request.scriptName || "") + (request.pathInfo || ""), 35 | query = request.queryString ? "?" + request.queryString : "", 36 | version = request.jsgi.version, 37 | status = String(response.status).substring(0,3), 38 | size = length === 0 ? "-" : "" + length, 39 | duration = now.getTime() - time.getTime(); 40 | 41 | var stringToLog = address+' - '+user+' ['+timestamp+'] "'+method+' '+path+query+' '+version+'" '+status+' '+size 42 | //stringToLog += ' '+duration; 43 | 44 | logger.log(stringToLog); 45 | } 46 | ); 47 | } 48 | } 49 | 50 | return response; 51 | }); 52 | } 53 | } 54 | 55 | CommonLogger.formatDate = function(date) { 56 | var d = date.getDate(), 57 | m = CommonLogger.MONTHS[date.getMonth()], 58 | y = date.getFullYear(), 59 | h = date.getHours(), 60 | mi = date.getMinutes(), 61 | s = date.getSeconds(); 62 | 63 | // TODO: better formatting 64 | return (d<10?"0":"")+d+"/"+m+"/"+y+" "+ 65 | (h<10?"0":"")+h+":"+(mi<10?"0":"")+mi+":"+(s<10?"0":"")+s; 66 | } 67 | 68 | CommonLogger.MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 69 | -------------------------------------------------------------------------------- /examples/comet.js: -------------------------------------------------------------------------------- 1 | var Jack = require("jack"), 2 | Promise = require("promise").Promise; 3 | 4 | 5 | var cometWorker = new (require("worker").SharedWorker)("chat-router", "chat-router"); 6 | 7 | var map = {}; 8 | // everyone who is listening 9 | var listeners = []; 10 | cometWorker.port.onmessage = function(event){ 11 | // got a message, route it to all the listeners 12 | var listener; 13 | for(var i = listeners.length; i-- > 0;){ 14 | listener = listeners[i]; 15 | try{ 16 | listener(event); 17 | } 18 | catch(e){ 19 | print(e); 20 | listeners.splice(i,1); 21 | } 22 | } 23 | } 24 | 25 | // an example of using setTimeout in our event-loop 26 | function randomMessage(){ 27 | setTimeout(function(){ 28 | cometWorker.port.postMessage("This is a random number " + Math.random() + " at a random time"); 29 | randomMessage(); 30 | },Math.random() * 10000); 31 | 32 | } 33 | randomMessage(); 34 | 35 | map["/"] = function(request) { 36 | var res = new Jack.Response(), 37 | message = reqObj.params("message"); 38 | 39 | if (message) { 40 | cometWorker.port.postMessage(message); 41 | 42 | res.write("sent '" + message + "' to clients"); 43 | } 44 | res.write('
'); 45 | res.write(''); 46 | res.write(''); 47 | res.write('
'); 48 | 49 | res.write('listen'); 50 | 51 | return res.finish(); 52 | } 53 | 54 | map["/listen"] = function(request) { 55 | var res = new Jack.Response(200, {"Transfer-Encoding":"chunked"}); 56 | return res.finish(function(response) { 57 | 58 | // HACK: Safari doesn't display chunked data until a certain number of bytes 59 | for (var i = 0; i < 10; i++) 60 | response.write("................................................................................................................................
"); 61 | 62 | var q = new MessageQueue(); 63 | queues.push(q); 64 | 65 | while (true) { 66 | var message = q.take(); 67 | response.write("received: " + message + "
"); 68 | } 69 | }); 70 | response = { 71 | status: 200, 72 | headers: {"Content-Type":"text/html", "Transfer-Encoding":"chunked"}, 73 | body: {forEach: function(callback){ 74 | // write will be called by the listener 75 | write = callback; 76 | }} 77 | }; 78 | return promise; // return a promise to indicate that it is not done yet 79 | } 80 | 81 | 82 | // apply the URLMap 83 | exports.app = Jack.URLMap(map); 84 | 85 | var timer, queue; 86 | // This a rhino-specific impl that creates a thread that queues up timer tasks. 87 | // Note that the tasks are still executed in the same thread asynchronously, 88 | // the timer thread is only used for queuing 89 | function setTimeout(callback, delay){ 90 | timer = timer || new java.util.Timer("JavaScript timer thread", true); 91 | queue = queue || require("event-loop"); 92 | 93 | timer.schedule(new java.util.TimerTask({ 94 | run: function(){ 95 | queue.enqueue(callback); 96 | } 97 | }), Math.floor(delay)); 98 | } -------------------------------------------------------------------------------- /lib/jack/dir.js: -------------------------------------------------------------------------------- 1 | var file = require("file"), 2 | sprintf = require("printf").sprintf, 3 | utils = require("./utils"), 4 | mimeType = require("./mime").mimeType; 5 | 6 | var DIR_FILE = 7 | '\n\ 8 | %s\n\ 9 | %s\n\ 10 | %s\n\ 11 | %s\n\ 12 | '; 13 | 14 | var DIR_PAGE = 15 | '\n\ 16 | %s\n\ 17 | \n\ 18 | \n\ 25 | \n\ 26 |

%s

\n\ 27 |
\n\ 28 | \n\ 29 | \n\ 30 | \n\ 31 | \n\ 32 | \n\ 33 | \n\ 34 | \n\ 35 | %s\n\ 36 |
NameSizeTypeLast Modified
\n\ 37 |
\n\ 38 | \n'; 39 | 40 | exports.Directory = function(root, app) { 41 | root = file.absolute(root); 42 | app = app || require("./file").File(root); 43 | return function(env) { 44 | var scriptName = utils.unescape(env.scriptName), 45 | pathInfo = utils.unescape(env.pathInfo); 46 | 47 | if (pathInfo.indexOf("..") >= 0) 48 | return utils.responseForStatus(403); 49 | 50 | var path = file.join(root, pathInfo); 51 | 52 | if (file.isReadable(path)) { 53 | if (file.isFile(path)) { 54 | return app(env); 55 | } 56 | else if (file.isDirectory(path)) { 57 | var body = generateListing(root, pathInfo, scriptName).toByteString("UTF-8"); 58 | return { 59 | status : 200, 60 | headers : { "Content-Type" : "text/html; charset=utf-8", "Content-Length" : String(body.length)}, 61 | body : [body] 62 | }; 63 | } 64 | } 65 | return utils.responseForStatus(404, pathInfo); 66 | } 67 | } 68 | 69 | function generateListing(root, pathInfo, scriptName) { 70 | var filesData = [["../","Parent Directory","","",""]]; 71 | 72 | var dirname = file.join(root, pathInfo), 73 | list = file.list(dirname); 74 | 75 | filesData.push.apply(filesData, list.map(function(basename) { 76 | var path = file.join(dirname, basename), 77 | ext = file.extension(basename), 78 | isDir = file.isDirectory(path), 79 | url = file.join(scriptName, pathInfo, basename), 80 | size = isDir ? "-" : byteSizeFormat(file.size(path)), 81 | type = isDir ? "directory" : mimeType(ext), 82 | mtime = file.mtime(path).toUTCString(); 83 | 84 | if (isDir) { 85 | url = url + "/"; 86 | basename = basename + "/"; 87 | } 88 | 89 | return [url, basename, size, type, mtime]; 90 | })); 91 | 92 | var files = filesData.map(function(file) { 93 | return sprintf(DIR_FILE, file[0], file[1], file[2], file[3], file[4]); 94 | }).join("\n"); 95 | 96 | return sprintf(DIR_PAGE, pathInfo, pathInfo, files); 97 | } 98 | 99 | function byteSizeFormat(size) { 100 | var tier = size > 0 ? Math.floor(Math.log(size) / Math.log(1024)) : 0; 101 | return sprintf(["%dB","%.1fK","%.1fM","%.1fG","%.1fT"][tier], size / Math.pow(1024, tier)); 102 | } 103 | 104 | exports.app = exports.Directory(file.cwd()); 105 | -------------------------------------------------------------------------------- /lib/jack/auth/digest/handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | var update = require('hash').Hash.update, 11 | md5 = require('md5'), 12 | base16 = require("base16"), 13 | AbstractHandler = require('jack/auth/abstract/handler').Handler, 14 | DigestRequest = require("jack/auth/digest/request").Request, 15 | DigestParams = require('jack/auth/digest/params'), 16 | DigestNonce = require('jack/auth/digest/nonce'); 17 | 18 | ///////////////// 19 | // Digest helpers 20 | ///////////////// 21 | 22 | var base16md5 = exports.base16md5 = function(s) { 23 | return base16.encode(md5.hash(s)); 24 | }; 25 | 26 | var H = base16md5; 27 | 28 | var qopSupported = ['auth']; // 'auth-int' is only implemented by Opera and Konquerer, 29 | 30 | var A1 = function(request, password) { 31 | return [request.username, request.realm, password].join(':'); 32 | }; 33 | 34 | var A2 = function(request) { 35 | return [request.method, request.uri].join(':'); 36 | }; 37 | 38 | var digest = exports.digest = function(request, password) { 39 | return H([H(A1(request, password)), request.nonce, request.nc, request.cnonce, request.qop, H(A2(request))].join(':')); 40 | }; 41 | 42 | ///////////////// 43 | // Digest handler 44 | ///////////////// 45 | 46 | var Handler = exports.Handler = function(params) { 47 | AbstractHandler.call(this, params); 48 | } 49 | 50 | Handler.prototype = update(Object.create(AbstractHandler.prototype), { 51 | 52 | // generate() returns a Digest Auth JSGI handler for the app 53 | generate: function(app) { 54 | 55 | var self = this; 56 | 57 | return function(env) { 58 | 59 | var request = new DigestRequest(env); 60 | 61 | if (!request.authorizationKey()) return self.Unauthorized(); 62 | if (!request.isDigest()) return self.BadRequest; 63 | if (!request.isCorrectUri()) return self.BadRequest; 64 | 65 | if (!self.isValidQOP(request)) return self.BadRequest; 66 | if (!self.isValidOpaque(request)) return self.Unauthorized(); 67 | if (!self.isValidDigest(request)) return self.Unauthorized(); 68 | 69 | if (!request.decodeNonce().isValid()) return self.Unauthorized(); 70 | if (!request.decodeNonce().isFresh()) return self.Unauthorized(self.issueChallenge({stale: true})); 71 | 72 | env['REMOTE_USER'] = request.username; 73 | return app(env); 74 | } 75 | }, 76 | 77 | params: function(options) { 78 | return update(options || {}, { 79 | realm: this.realm, 80 | nonce: new DigestNonce.Nonce().toString(), 81 | opaque: H(this.opaque), 82 | qop: qopSupported.join(',') 83 | }); 84 | }, 85 | 86 | issueChallenge: function(options) { 87 | return "Digest " + DigestParams.toString(this.params(options)); 88 | }, 89 | 90 | isValidQOP: function(request) { 91 | return qopSupported.indexOf(request.qop) != -1; 92 | }, 93 | 94 | isValidOpaque: function(request) { 95 | return H(this.opaque) == request.opaque; 96 | }, 97 | 98 | isValidDigest: function(request) { 99 | return digest(request, this.getPassword(request.username)) == request.response; 100 | } 101 | }); 102 | 103 | /******************************************************** 104 | * Digest Auth Middleware 105 | ********************************************************/ 106 | 107 | exports.Middleware = function(app, options) { 108 | return new Handler(options).generate(app); 109 | }; -------------------------------------------------------------------------------- /lib/jack/mock.js: -------------------------------------------------------------------------------- 1 | var URI = require("uri").URI, 2 | ByteString = require("binary").ByteString, 3 | ByteIO = require("io").ByteIO; 4 | 5 | var Lint = require("jack/lint").Lint; 6 | 7 | /** 8 | * MockRequest helps testing your Jack application without actually using HTTP. 9 | * 10 | * After performing a request on a URL with get/post/put/delete, it returns a 11 | * MockResponse with useful helper methods for effective testing. 12 | */ 13 | var MockRequest = exports.MockRequest = function(app) { 14 | if(!(this instanceof MockRequest)) 15 | return new MockRequest(app); 16 | 17 | this.app = app; 18 | } 19 | 20 | MockRequest.prototype.GET = function(uri, opts) { 21 | return this.request("GET", uri, opts); 22 | } 23 | 24 | MockRequest.prototype.POST = function(uri, opts) { 25 | return this.request("POST", uri, opts); 26 | } 27 | 28 | MockRequest.prototype.PUT = function(uri, opts) { 29 | return this.request("PUT", uri, opts); 30 | } 31 | 32 | MockRequest.prototype.DELETE = function(uri, opts) { 33 | return this.request("DELETE", uri, opts); 34 | } 35 | 36 | MockRequest.prototype.request = function(method, uri, opts) { 37 | opts = opts || {}; 38 | 39 | var request = MockRequest.requestFor(method, uri, opts), 40 | app = this.app; 41 | 42 | if (opts.lint) 43 | app = Lint(app) 44 | 45 | return new MockResponse(app(request), request.jsgi.errors); 46 | } 47 | 48 | MockRequest.requestFor = function(method, uri, opts) { 49 | opts = opts || {}; 50 | 51 | var uri = new URI(uri); 52 | 53 | // DEFAULT_ENV 54 | var request = { 55 | jsgi: { 56 | version: [0,3], 57 | input: opts["jsgi.input"] || new ByteIO(new ByteString()), 58 | errors: opts["jsgi.errors"] || "", 59 | multithread: false, 60 | multiprocess: true, 61 | runOnce: false 62 | }, 63 | headers: {}, 64 | body: opts.body 65 | }; 66 | 67 | request.method = method || "GET"; 68 | request["HTTP_HOST"] = uri.host || "example.org"; 69 | request["SERVER_NAME"] = uri.host || "example.org"; 70 | request["SERVER_PORT"] = (uri.port || 80).toString(10); 71 | request.queryString = uri.query || ""; 72 | request.pathInfo = uri.path || "/"; 73 | request["jsgi.url_scheme"] = uri.scheme || "http"; 74 | 75 | request["SCRIPT_NAME"] = opts["SCRIPT_NAME"] || ""; 76 | if(request.body){ 77 | request.headers["content-length"] = request.body.length.toString(10); 78 | } 79 | 80 | // FIXME: JS can only have String keys unlike Ruby, so we're dumping all opts into the request here. 81 | for (var i in opts) 82 | request[i] = opts[i]; 83 | 84 | // FIXME: 85 | //if (typeof request["jsgi.body"] == "string") 86 | // request["jsgi.body"] = StringIO(request["jsgi.body"]) 87 | 88 | return request; 89 | } 90 | 91 | /** 92 | * MockResponse provides useful helpers for testing your apps. Usually, you 93 | * don't create the MockResponse on your own, but use MockRequest. 94 | */ 95 | var MockResponse = exports.MockResponse = function(response, errors) { 96 | if(this === global){ 97 | throw new Error("MockResponse must be instantiated"); 98 | } 99 | if(!(this instanceof MockResponse)) 100 | return new MockResponse(response, errors); 101 | 102 | this.status = response.status; 103 | this.headers = response.headers; 104 | 105 | var body = ""; 106 | response.body.forEach(function(chunk) { 107 | body += chunk.toByteString().decodeToString(); 108 | }); 109 | this.body = body; 110 | 111 | this.errors = errors || ""; 112 | }; 113 | 114 | MockResponse.prototype.match = function(regex) { 115 | return this.body.match(new RegExp(regex)); 116 | }; 117 | 118 | -------------------------------------------------------------------------------- /lib/jack/handler/worker-delegator.js: -------------------------------------------------------------------------------- 1 | // Creates a pool of workers and delegates requests to workers as they become idle 2 | 3 | var Worker = require("worker").Worker, 4 | HTTP_STATUS_CODES = require("../utils").HTTP_STATUS_CODES, 5 | spawn = require("worker-engine").spawn, 6 | requestQueue; 7 | exports.enqueue = function(request, response){ 8 | // just puts them in the queue 9 | return requestQueue.offer([request, response]); 10 | 11 | } 12 | 13 | exports.createQueue = function(options){ 14 | var workerPoolSize = options.workerPoolSize || 5; 15 | var requestQueueCapacity = options.requestQueueCapacity || 20; 16 | // our request queue for delegating requests to workers 17 | requestQueue = new java.util.concurrent.LinkedBlockingQueue(requestQueueCapacity); 18 | }; 19 | exports.createWorkers = function(workerModule, options) { 20 | var maxWorkerPoolSize = options.maxWorkerPoolSize || 5; 21 | options.server = workerModule; 22 | // create all are workers for servicing requests 23 | var workers = []; 24 | var workerIds = []; 25 | function addWorker(){ 26 | var i = workers.length; 27 | var newWorker = new Worker("jack/handler/" + workerModule, workerIds[i] = "Jack worker " + i); 28 | var optionsCopy = {}; 29 | for(var key in options){ 30 | optionsCopy[key] = options[key]; 31 | } 32 | if(i == 0){ 33 | optionsCopy.firstWorker = true; 34 | } 35 | newWorker.__enqueue__("onstart", [optionsCopy]); 36 | workers.forEach(function(worker){ 37 | /* worker.postMessage({ 38 | method:"subscribe", 39 | body:{ 40 | target: "worker://" + newWorker.name 41 | } 42 | }); 43 | newWorker.postMessage({ 44 | method:"subscribe", 45 | body:{ 46 | target: "worker://" + worker.name 47 | } 48 | });*/ 49 | var connectionA = { 50 | send: function(message){ 51 | newWorker.__enqueue__("onsiblingmessage", [message, connectionB]); 52 | } 53 | }; 54 | var connectionB = { 55 | send: function(message){ 56 | worker.__enqueue__("onsiblingmessage", [message, connectionA]); 57 | } 58 | }; 59 | worker.__enqueue__("onnewworker", [connectionA]); 60 | newWorker.__enqueue__("onnewworker", [connectionB]); 61 | }); 62 | workers[i] = newWorker; 63 | } 64 | addWorker(); // create at least one to start with 65 | 66 | // our event queue 67 | var eventQueue = require("event-loop"); 68 | 69 | /* onmessage = function(e){ 70 | if(typeof e.data == "object"){ 71 | if(e.data.method === "get" && e.data.pathInfo === "/workers"){ 72 | workerListeners.push(newWorkers); 73 | newWorkers(workerIds); 74 | function newWorkers(workerIds){ 75 | e.port.postMessage({ 76 | source: "/workers", 77 | body:workerIds 78 | }); 79 | } 80 | } 81 | } 82 | };*/ 83 | requestProcess: 84 | while(true){ 85 | var requestResponse = requestQueue.take(); // get the next request 86 | while(true){ 87 | for(var i = 0; i < workers.length; i++){ 88 | var worker = workers[i]; 89 | if(worker && !worker.hasPendingEvents()){ 90 | worker.__enqueue__("onrequest", requestResponse); 91 | continue requestProcess; 92 | } 93 | } 94 | // no available workers, 95 | // create another worker if we are under our limit 96 | if(workers.length < maxWorkerPoolSize){ 97 | addWorker(); 98 | } 99 | // block for events (only waiting for onidle events) 100 | eventQueue.processNextEvent(true); 101 | } 102 | } 103 | 104 | } 105 | 106 | -------------------------------------------------------------------------------- /lib/jack/showstatus.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack showstatus.rb 7 | * http://github.com/rack/rack 8 | * 9 | * escapeHTML() adapted from the nitro framework 10 | * http://github.com/gmosx/nitro 11 | */ 12 | 13 | var Request = require("./request").Request, 14 | HTTP_STATUS_CODES = require("./utils").HTTP_STATUS_CODES, 15 | sprintf = require("printf").sprintf; 16 | 17 | var escapeHTML = function(str) { 18 | return str ? str.replace(/&/g, "&").replace(//g, ">") : ""; 19 | }; 20 | 21 | var ShowStatus = function(app) { 22 | return function(request) { 23 | return when(app(request), function(response) { 24 | // client or server error, or explicit message 25 | if ((response.status >= 400 && response.body.length == 0) || request.env.jack.showstatus.detail) { 26 | var req = new Request(request); 27 | var status = HTTP_STATUS_CODES[response.status] || String(response.status); 28 | 29 | response.body = [ 30 | sprintf(template, 31 | escapeHTML(status), 32 | escapeHTML(req.scriptName() + req.pathInfo()), 33 | escapeHTML(status), 34 | response.status, 35 | escapeHTML(req.requestMethod()), 36 | escapeHTML(req.uri()), 37 | escapeHTML(request.env.jack.showstatus.detail || status) 38 | ) 39 | ]; 40 | 41 | response.headers["content-type"] = "text/html"; 42 | } 43 | return response; 44 | }); 45 | } 46 | }; 47 | 48 | exports.ShowStatus = function(app) { 49 | return require("jack").ContentLength(ShowStatus(app)); 50 | } 51 | 52 | /* 53 | template adapted from Django 54 | Copyright (c) 2005, the Lawrence Journal-World 55 | Used under the modified BSD license: 56 | http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 57 | */ 58 | 59 | var template ='\ 60 | \ 61 | \ 62 | \ 63 | \ 64 | %s at %s\ 65 | \ 66 | \ 83 | \ 84 | \ 85 |
\ 86 |

%s (%d)

\ 87 | \ 88 | \ 89 | \ 90 | \ 91 | \ 92 | \ 93 | \ 94 | \ 95 | \ 96 |
Request Method:%s
Request URL:%s
\ 97 |
\ 98 |
\ 99 |

%s

\ 100 |
\ 101 | \ 102 |
\ 103 |

\ 104 | You are seeing this error because you use Jack.ShowStatus.\ 105 |

\ 106 |
\ 107 | \ 108 | '; 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | JSGI & Jack 3 | =========== 4 | 5 | JSGI is a web server interface specification for JavaScript, inspired by Ruby's Rack ([http://rack.rubyforge.org/](http://rack.rubyforge.org/)) and Python's WSGI ([http://www.wsgi.org/](http://www.wsgi.org/)). It provides a common API for connecting JavaScript frameworks and applications to webservers. 6 | 7 | Jack is a collection of JSGI compatible handlers (connect web servers to JavaScript web application/frameworks), middleware (intercept and manipulate requests to add functionality), and other utilities (to help build middleware, frameworks, and applications). 8 | 9 | 10 | ### Homepage: 11 | 12 | * [http://jackjs.org/](http://jackjs.org/) 13 | * [http://narwhaljs.org/](http://narwhaljs.org/) 14 | 15 | ### Source & Download: 16 | 17 | * [http://github.com/tlrobinson/jack/](http://github.com/tlrobinson/jack/) 18 | * [http://github.com/tlrobinson/narwhal/](http://github.com/tlrobinson/narwhal/) 19 | 20 | ### Mailing list: 21 | 22 | * [http://groups.google.com/group/narwhaljs](http://groups.google.com/group/narwhaljs) 23 | 24 | ### IRC: 25 | 26 | * [\#narwhal on irc.freenode.net](http://webchat.freenode.net/?channels=narwhal) 27 | 28 | 29 | JSGI Specification 30 | ------------------ 31 | 32 | View the [JSGI specification](http://jackjs.org/jsgi-spec.html). 33 | 34 | 35 | Example JSGI Application 36 | ------------------------ 37 | 38 | function(env) { 39 | return { 40 | status : 200, 41 | headers : {"Content-Type":"text/plain"}, 42 | body : ["Hello world!"] 43 | }; 44 | } 45 | 46 | 47 | JSGI vs. Rack 48 | ------------- 49 | 50 | JSGI applications are simply functions, rather than objects that respond to the "call" method. The body must have a `forEach` method which yields objects which have a `toByteString` method, as opposed to Strings. 51 | 52 | 53 | JSGI vs. WSGI 54 | ------------- 55 | 56 | WSGI uses a `start_response` function to set the HTTP status code and headers, rather than returning them in an array. JSGI is similar to WSGI 2.0: [http://www.wsgi.org/wsgi/WSGI_2.0](http://www.wsgi.org/wsgi/WSGI_2.0). 57 | 58 | 59 | Contributors 60 | ------------ 61 | 62 | * [Tom Robinson](http://tlrobinson.net/) 63 | * [George Moschovitis](http://www.gmosx.com/) 64 | * [Kris Kowal](http://askawizard.blogspot.com/) 65 | * Neville Burnell 66 | * [Isaac Z. Schlueter](http://blog.izs.me/) 67 | * Jan Varwig 68 | * [Irakli Gozalishvili](http://rfobic.wordpress.com/) 69 | * [Kris Zyp](http://www.sitepen.com/blog/author/kzyp/) 70 | * [Kevin Dangoor](http://www.blueskyonmars.com/) 71 | * Antti Holvikari 72 | * Tim Schaub 73 | 74 | 75 | Acknowledgments 76 | --------------- 77 | 78 | This software was influenced by Rack, written by Christian Neukirchen. 79 | 80 | [http://rack.rubyforge.org/](http://rack.rubyforge.org/) 81 | 82 | 83 | License 84 | ------- 85 | 86 | Copyright (c) 2009 Thomas Robinson <[280north.com](http://280north.com/)\> 87 | 88 | Permission is hereby granted, free of charge, to any person obtaining a copy 89 | of this software and associated documentation files (the "Software"), to 90 | deal in the Software without restriction, including without limitation the 91 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 92 | sell copies of the Software, and to permit persons to whom the Software is 93 | furnished to do so, subject to the following conditions: 94 | 95 | The above copyright notice and this permission notice shall be included in 96 | all copies or substantial portions of the Software. 97 | 98 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 99 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 100 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 101 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 102 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 103 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 104 | 105 | -------------------------------------------------------------------------------- /docs/jack-auth.md: -------------------------------------------------------------------------------- 1 | Jack.Auth 2 | ========= 3 | 4 | JSGI authorization handlers for Jack and Narwhal. 5 | 6 | 7 | Status 8 | ------ 9 | 10 | * Basic: Completed 11 | * Digest: Completed. 12 | * OpenID: Outstanding 13 | 14 | 15 | History 16 | ------- 17 | 18 | * 2009-09-10 V0.5 Merged with Jack. Tidy up names, params, exports. These changes will break existing code. 19 | * 2009-09-09 V0.4 Digest Authentication. 20 | * 2009-09-04 V0.3 Conform to Jack response specification change from array to object. BasicMiddleware() API change. 21 | * 2009-08-26 V0.2 Changed authenticator() parameters. 22 | * 2009-08-20 V0.1 First Release. Basic Authentication. 23 | 24 | 25 | Usage: Basic Authentication 26 | --------------------------- 27 | 28 | var BasicAuth = require("jack/auth/basic/handler").Middleware; 29 | 30 | var myApp = function(env) { 31 | return { 32 | status: 200, 33 | headers: {"Content-Type": "text/plain"}, 34 | body: ["Hello Admin!"] 35 | } 36 | } 37 | 38 | // The JSGI handler is generated by the call to Middleware(app, params) 39 | // The params are: 40 | // realm: a string declaring the authentication realm 41 | // isValid(request): a function which takes an object which exposes username and password and returns 42 | // true or false if the pair is accepted or rejected 43 | 44 | exports.app = BasicAuth(myApp, { 45 | realm: "my realm", 46 | isValid: function(request) { 47 | if (request.username == 'admin' && request.password == 'pass') 48 | return true; //allow 49 | return false; //deny 50 | } 51 | }); 52 | 53 | 54 | Usage: Digest Authentication 55 | ---------------------------- 56 | 57 | var DigestAuth = require("jack/auth/digest/handler").Middleware; 58 | 59 | var myApp = function(env) { 60 | return { 61 | status: 200, 62 | headers: {"Content-Type": "text/plain"}, 63 | body: ["Hello Admin!"] 64 | } 65 | } 66 | 67 | // The JSGI handler is generated by the call to Middleware(app, params) 68 | // The params are: 69 | // realm: a string declaring the authentication realm 70 | // opaque: a secret hashed and passed to the client 71 | // getPassword(username): returns the password for a username 72 | 73 | exports.app = DigestAuth(myApp, { 74 | realm: "my realm", 75 | opaque: "this-is-a-secret", 76 | getPassword: function(username) { 77 | return {'admin': 'password'}[username]; 78 | } 79 | }); 80 | 81 | 82 | Contributors 83 | ------------ 84 | 85 | * [Neville Burnell][2] 86 | 87 | 88 | Acknowledgments 89 | --------------- 90 | 91 | This software was inspired by [Rack::Auth][1] 92 | 93 | [1]:http://github.com/rack/rack 94 | [2]:http://github.com/nevilleburnell 95 | 96 | 97 | License 98 | ------- 99 | 100 | Copyright (c) 2009 Neville Burnell 101 | 102 | Permission is hereby granted, free of charge, to any person obtaining a copy 103 | of this software and associated documentation files (the "Software"), to 104 | deal in the Software without restriction, including without limitation the 105 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 106 | sell copies of the Software, and to permit persons to whom the Software is 107 | furnished to do so, subject to the following conditions: 108 | 109 | The above copyright notice and this permission notice shall be included in 110 | all copies or substantial portions of the Software. 111 | 112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 113 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 114 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 115 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 116 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 117 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 118 | 119 | -------------------------------------------------------------------------------- /lib/jack/querystring.js: -------------------------------------------------------------------------------- 1 | // Query String Utilities 2 | 3 | var DEFAULT_SEP = "&"; 4 | var DEFAULT_EQ = "="; 5 | 6 | exports.unescape = function (str, decodeSpaces) { 7 | return decodeURIComponent(decodeSpaces ? str.replace(/\+/g, " ") : str); 8 | }; 9 | 10 | exports.escape = function (str) { 11 | return encodeURIComponent(str); 12 | }; 13 | 14 | exports.toQueryString = function (obj, sep, eq, name) { 15 | sep = sep || DEFAULT_SEP; 16 | eq = eq || DEFAULT_EQ; 17 | if (isA(obj, null) || isA(obj, undefined)) { 18 | return name ? encodeURIComponent(name) + eq : ''; 19 | } 20 | if (isNumber(obj) || isString(obj)) { 21 | return encodeURIComponent(name) + eq + encodeURIComponent(obj); 22 | } 23 | if (isA(obj, [])) { 24 | var s = []; 25 | name = name+'[]'; 26 | for (var i = 0, l = obj.length; i < l; i ++) { 27 | s.push( exports.toQueryString(obj[i], sep, eq, name) ); 28 | } 29 | return s.join(sep); 30 | } 31 | // now we know it's an object. 32 | var s = []; 33 | var begin = name ? name + '[' : ''; 34 | var end = name ? ']' : ''; 35 | for (var i in obj) if (obj.hasOwnProperty(i)) { 36 | var n = begin + i + end; 37 | s.push(exports.toQueryString(obj[i], sep, eq, n)); 38 | } 39 | return s.join(sep); 40 | }; 41 | 42 | exports.parseQuery = function(qs, sep, eq) { 43 | return qs 44 | .split(sep||DEFAULT_SEP) 45 | .map(pieceParser(eq||DEFAULT_EQ)) 46 | .reduce(mergeParams); 47 | }; 48 | 49 | // Parse a key=val string. 50 | // These can get pretty hairy 51 | // example flow: 52 | // parse(foo[bar][][bla]=baz) 53 | // return parse(foo[bar][][bla],"baz") 54 | // return parse(foo[bar][], {bla : "baz"}) 55 | // return parse(foo[bar], [{bla:"baz"}]) 56 | // return parse(foo, {bar:[{bla:"baz"}]}) 57 | // return {foo:{bar:[{bla:"baz"}]}} 58 | var pieceParser = function (eq) { 59 | return function parsePiece (key, val) { 60 | if (arguments.length !== 2) { 61 | // key=val, called from the map/reduce 62 | key = key.split(eq); 63 | return parsePiece( 64 | exports.unescape(key.shift(), true), exports.unescape(key.join(eq), true) 65 | ); 66 | } 67 | key = key.replace(/^\s+|\s+$/g, ''); 68 | if (isString(val)) val = val.replace(/^\s+|\s+$/g, ''); 69 | var sliced = /(.*)\[([^\]]*)\]$/.exec(key); 70 | if (!sliced) { 71 | var ret = {}; 72 | if (key) ret[key] = val; 73 | return ret; 74 | } 75 | // ["foo[][bar][][baz]", "foo[][bar][]", "baz"] 76 | var tail = sliced[2], head = sliced[1]; 77 | 78 | // array: key[]=val 79 | if (!tail) return parsePiece(head, [val]); 80 | 81 | // obj: key[subkey]=val 82 | var ret = {}; 83 | ret[tail] = val; 84 | return parsePiece(head, ret); 85 | }; 86 | }; 87 | 88 | // the reducer function that merges each query piece together into one set of params 89 | function mergeParams (params, addition) { 90 | return ( 91 | // if it's uncontested, then just return the addition. 92 | (!params) ? addition 93 | // if the existing value is an array, then concat it. 94 | : (isA(params, [])) ? params.concat(addition) 95 | // if the existing value is not an array, and either are not objects, arrayify it. 96 | : (!isA(params, {}) || !isA(addition, {})) ? [params].concat(addition) 97 | // else merge them as objects, which is a little more complex 98 | : mergeObjects(params, addition) 99 | ); 100 | }; 101 | 102 | // Merge two *objects* together. If this is called, we've already ruled 103 | // out the simple cases, and need to do the for-in business. 104 | function mergeObjects (params, addition) { 105 | for (var i in addition) if (i && addition.hasOwnProperty(i)) { 106 | params[i] = mergeParams(params[i], addition[i]); 107 | } 108 | return params; 109 | }; 110 | 111 | // duck typing 112 | function isA (thing, canon) { 113 | return ( 114 | // truthiness. you can feel it in your gut. 115 | (!thing === !canon) 116 | // typeof is usually "object" 117 | && typeof(thing) === typeof(canon) 118 | // check the constructor 119 | && Object.prototype.toString.call(thing) === Object.prototype.toString.call(canon) 120 | ); 121 | }; 122 | function isNumber (thing) { 123 | return typeof(thing) === "number" && isFinite(thing); 124 | }; 125 | function isString (thing) { 126 | return typeof(thing) === "string"; 127 | }; 128 | -------------------------------------------------------------------------------- /lib/jack/handler/shttpd.js: -------------------------------------------------------------------------------- 1 | // handler for SHTTPD/Mongoose (http://code.google.com/p/mongoose/) 2 | 3 | var IO = require("io").IO, 4 | HashP = require("hashp").HashP; 5 | 6 | exports.run = function(app, options) { 7 | var options = options || {}, 8 | port = options["port"] || 8080, 9 | shttpd = options["shttpd"] || net.http.server.shttpd; 10 | 11 | var server = new shttpd.Server(port); 12 | 13 | server.registerURI( 14 | "/*", 15 | function (request) { 16 | process(app, request, shttpd); 17 | } 18 | ); 19 | 20 | print("Jack is starting up using SHTTPD on port " + port); 21 | 22 | while (true) { 23 | server.processRequests(); 24 | } 25 | } 26 | 27 | // Apparently no way to enumerate ENV or headers so we have to check against a list of common ones for now. Sigh. 28 | 29 | var ENV_KEYS = [ 30 | "SERVER_SOFTWARE", "SERVER_NAME", "GATEWAY_INTERFACE", "SERVER_PROTOCOL", 31 | "SERVER_PORT", "REQUEST_METHOD", "PATH_INFO", "PATH_TRANSLATED", "SCRIPT_NAME", 32 | "QUERY_STRING", "REMOTE_HOST", "REMOTE_ADDR", "AUTH_TYPE", "REMOTE_USER", "REMOTE_IDENT", 33 | "CONTENT_TYPE", "CONTENT_LENGTH", "HTTP_ACCEPT", "HTTP_USER_AGENT", 34 | "REQUEST_URI" 35 | ]; 36 | 37 | var HEADER_KEYS = [ 38 | "Accept", "Accept-Charset", "Accept-Encoding", "Accept-Language", "Accept-Ranges", 39 | "Authorization", "Cache-Control", "Connection", "Cookie", "Content-Type", "Date", 40 | "Expect", "Host", "If-Match", "If-Modified-Since", "If-None-Match", "If-Range", 41 | "If-Unmodified-Since", "Max-Forwards", "Pragma", "Proxy-Authorization", "Range", 42 | "Referer", "TE", "Upgrade", "User-Agent", "Via", "Warn" 43 | ]; 44 | 45 | var process = function(app, request, shttpd) { 46 | try { 47 | var env = {}; 48 | 49 | var key, value; 50 | 51 | ENV_KEYS.forEach(function(key) { 52 | if (value = request.getEnv(key)) 53 | env[key] = value; 54 | }); 55 | 56 | HEADER_KEYS.forEach(function(key) { 57 | if (value = request.getHeader(key)) { 58 | key = key.replace(/-/g, "_").toUpperCase(); 59 | if (!key.match(/(CONTENT_TYPE|CONTENT_LENGTH)/i)) 60 | key = "HTTP_" + key; 61 | env[key] = value; 62 | } 63 | }); 64 | 65 | var hostComponents = env["HTTP_HOST"].split(":") 66 | if (env["SERVER_NAME"] === undefined && hostComponents[0]) 67 | env["SERVER_NAME"] = hostComponents[0]; 68 | if (env["SERVER_PORT"] === undefined && hostComponents[1]) 69 | env["SERVER_PORT"] = hostComponents[1]; 70 | 71 | if (env["QUERY_STRING"] === undefined) 72 | env["QUERY_STRING"] = ""; 73 | 74 | if (env["PATH_INFO"] === undefined) 75 | env["PATH_INFO"] = env["REQUEST_URI"]; 76 | 77 | if (env["SERVER_PROTOCOL"] === undefined) 78 | env["SERVER_PROTOCOL"] = "HTTP/1.1"; 79 | 80 | env["HTTP_VERSION"] = env["SERVER_PROTOCOL"]; // legacy 81 | 82 | if (env["CONTENT_LENGTH"] === undefined) 83 | env["CONTENT_LENGTH"] = "0"; 84 | 85 | if (env["CONTENT_TYPE"] === undefined) 86 | env["CONTENT_TYPE"] = "text/plain"; 87 | 88 | env["jsgi.version"] = [0,2]; 89 | env["jsgi.input"] = null; // FIXME 90 | env["jsgi.errors"] = system.stderr; 91 | env["jsgi.multithread"] = false; 92 | env["jsgi.multiprocess"] = true; 93 | env["jsgi.run_once"] = false; 94 | env["jsgi.url_scheme"] = "http"; // FIXME 95 | 96 | // call the app 97 | var response = app(env); 98 | 99 | // FIXME: can't set the response status or headers?! 100 | 101 | // set the status 102 | //response.status 103 | 104 | // set the headers 105 | //response.headers 106 | 107 | // output the body 108 | response.body.forEach(function(bytes) { 109 | request.print(bytes.toByteString("UTF-8").decodeToString("UTF-8")); 110 | }); 111 | 112 | } catch (e) { 113 | print("Exception! " + e); 114 | } finally { 115 | request.setFlags(shttpd.END_OF_OUTPUT); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/jack/narwhal.js: -------------------------------------------------------------------------------- 1 | var Request = require("./request").Request, 2 | Response = require("./response").Response; 3 | 4 | exports.app = function(request) { 5 | var req = new Request(request), 6 | narwhal = "", 7 | href = ""; 8 | 9 | if (req.GET("narwhals")) { 10 | narwhal = NARWHALS_FLASH; 11 | } 12 | else if (req.GET("flip") === "crash") { 13 | throw new Error("Narwhal crashed"); 14 | } 15 | else if (req.GET("flip") === "left") { 16 | narwhal = NARWHAL_LEFT; 17 | href = "?flip=right"; 18 | } 19 | else { 20 | narwhal = NARWHAL_RIGHT; 21 | href = "?flip=left"; 22 | } 23 | 24 | var res = new Response(); 25 | 26 | res.write("Narwhalicious!"); 27 | res.write("

flip!

"); 28 | res.write("

more!

"); 29 | res.write("

crash!

"); 30 | res.write("
");
31 |     res.write(narwhal);
32 |     res.write("
"); 33 | 34 | return res.finish(); 35 | } 36 | 37 | var NARWHAL_LEFT = " ,\n ,f\n ,t,\n ,:L.\n tL,\n :EDDEKL :Lt\n DDDDLGKKKK ,,tD\n ,GDDfi.itLKKEKL tEi\n DDEEf,,tfLLDEEDL,D\n .GEDEf,itLLfDLDLDDfD\n DDEDLf,,fLLGLLDLDti:DL\n DGDDGL,tttLDLDLfttttiLD\n GDDLLt,fLLLDLLtLi,ttfLG\n GGDGt,tLLLDfftii,i,ttLf\n DGLLtttftftttf,,tttitLt\n DEtftttLffttttii ttfLfj\n .DLtittftLftt,,i,,itLfLj\n DGL;t,tftiti,,,,,,tLLLt\n DGGttttttii,,,,,:,tttDG\n ,DLtjtiitii,,:,:,,t ,tG:\n DDjttttt,ii,,,,:::t:ttL\n ;GLjtttti,i,,, ,,LG,,ft\n DDLttftttti;,,ifDLDtiit\n EGLjtjftt,,,ifLt DLt,:\n DGfffijittfftt .DLLt\n:DGfjffftfLft EEDf\n:EGfftjjLLj EED\n:DGfLfjLGG ;E,\n GGfffLLL\n DGffLDf\n DGLfGL.\n fGLfGL\n DGLDL\n EGGGG\n DLGG\n EGLL\n ELG\n EEDKDGEE\n jKEKKKK\n EEKKKK\n DEE\n .EEKG\n Lf"; 38 | 39 | var NARWHAL_RIGHT = ",\nf,\n ,t,\n .L:,\n ,Lt\n tL: LKEDDE:\n Dt,, KKKKGLDDDD\n iEt LKEKKLti.ifDDG,\n D,LDEEDLLft,,fEEDD\n DfDDLDLDfLLti,fEDEG.\n LD:itDLDLLGLLf,,fLDEDD\n DLittttfLDLDLttt,LGDDGD\n GLftt,iLtLLDLLLf,tLLDDG\n fLtt,i,iitffDLLLt,tGDGG\n tLtittt,,ftttftftttLLGD\n jfLftt iittttffLtttftED\n jLfLti,,i,,ttfLtfttitLD.\n tLLLt,,,,,,ititft,t;LGD\n GDttt,:,,,,,iittttttGGD\n :Gt, t,,:,:,,iitiitjtLD,\n Ltt:t:::,,,,ii,tttttjDD\n tf,,GL,, ,,,i,ittttjLG;\n tiitDLDfi,,;ittttfttLDD\n :,tLD tLfi,,,ttfjtjLGE\n tLLD. ttffttijifffGD\n fDEE tfLftfffjfGD:\n DEE jLLjjtffGE:\n ,E; GGLjfLfGD:\n LLLfffGG\n fDLffGD\n .LGfLGD\n LGfLGf\n LDLGD\n GGGGE\n GGLD\n LLGE\n GLE\n EEGDKDEE\n KKKKEKj\n KKKKEE\n EED\n GKEE.\n fL\n"; 40 | 41 | var NARWHALS_FLASH = ''; 42 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | var Jack = require("jack"); 2 | 3 | var map = {}; 4 | 5 | // an extremely simple Jack application 6 | map["/hello"] = function(request) { 7 | return { 8 | status : 200, 9 | headers : {"content-type":"text/plain"}, 10 | body : ["Hello from " + request.scriptName] 11 | }; 12 | } 13 | 14 | // 1/6th the time this app will throw an exception 15 | map["/httproulette"] = function(env) { 16 | // if you have the ShowExceptions middleware in the pipeline it will print the error. 17 | // otherwise the server/handler will print something 18 | if (Math.random() > 5/6) 19 | throw new Error("bam!"); 20 | 21 | return { 22 | status : 200, 23 | headers : {"content-type":"text/html"}, 24 | body : ['whew!
try again'] 25 | }; 26 | } 27 | 28 | var form = '

'; 29 | 30 | // an index page demonstrating using a Response object 31 | map["/"] = function(request) { 32 | var req = new Jack.Request(request), 33 | res = new Jack.Response(); 34 | 35 | res.write('hello ' + (req.GET("name") || form) + "
"); 36 | [ 37 | "hello", 38 | "httproulette", 39 | "drinkinggame", 40 | "narwhal", 41 | "stream", 42 | "stream1", 43 | "cookie", 44 | "examples", 45 | "info" 46 | ].forEach(function(item) { 47 | res.write('' + item + '
'); 48 | }); 49 | return res.finish(); 50 | } 51 | 52 | map["/drinkinggame"] = require("./statuscodedrinkinggame").app; 53 | 54 | map["/narwhal"] = Jack.Narwhal; 55 | 56 | // use the JSONP middleware on this one 57 | map["/jsontest"] = Jack.JSONP(function(request) { 58 | return { 59 | status : 200, 60 | headers : { "content-type" : "application/json" }, 61 | body : ["{ \"hello\" : \"world\" }"] 62 | }; 63 | }); 64 | 65 | map["/files"] = Jack.File("."); 66 | 67 | map["/stream"] = function(request) { 68 | return { 69 | status : 200, 70 | headers : {"content-type":"text/html", "transfer-encoding": "chunked"}, 71 | body : { forEach : function(write) { 72 | for (var i = 0; i < 50; i++) { 73 | java.lang.Thread.currentThread().sleep(100); 74 | write("hellohellohellohellohellohellohellohellohellohellohellohellohello
"); 75 | } 76 | }} 77 | }; 78 | } 79 | 80 | 81 | map["/stream1"] = function(request) { 82 | var res = new Jack.Response(200, {"transfer-encoding": "chunked"}); 83 | return res.finish(function(response) { 84 | for (var i = 0; i < 50; i++) { 85 | java.lang.Thread.currentThread().sleep(100); 86 | response.write("hellohellohellohellohellohellohellohellohellohellohellohellohello
"); 87 | } 88 | }); 89 | } 90 | 91 | map["/cookie"] = function(request) { 92 | var req = new Jack.Request(request), 93 | res = new Jack.Response(); 94 | 95 | var name = req.POST("name"); 96 | 97 | if (typeof name === "string") { 98 | res.write("setting name: " + name + "
"); 99 | res.setCookie("name", name); 100 | } 101 | 102 | var cookies = req.cookies(); 103 | if (cookies["name"]) 104 | response.write("previously saved name: " + cookies["name"] +"
") 105 | 106 | res.write('
'); 107 | res.write(''); 108 | res.write('
'); 109 | 110 | return response.finish(); 111 | } 112 | 113 | map["/info"] = function(request) { 114 | var req = new Jack.Request(request), 115 | res = new Jack.Response(200, { "content-type" : "text/plain" }); 116 | 117 | var params = req.params(); 118 | 119 | res.write("========================= params =========================\n"); 120 | 121 | for (var i in params) 122 | res.write(i + "=>" + params[i] + "\n") 123 | 124 | res.write("========================= env =========================\n"); 125 | 126 | for (var i in env) 127 | res.write(i + "=>" + env[i] + "\n") 128 | 129 | res.write("========================= system.env =========================\n"); 130 | 131 | for (var i in system.env) 132 | res.write(i + "=>" + system.env[i] + "\n") 133 | 134 | return res.finish(); 135 | } 136 | 137 | map["/examples"] = Jack.Directory("."); 138 | 139 | // middleware: 140 | 141 | // apply the URLMap 142 | exports.app = Jack.URLMap(map); 143 | -------------------------------------------------------------------------------- /tests/jack/auth/auth-basic-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | var assert = require("test/assert"), 11 | base64 = require("base64"), 12 | MockRequest = require("jack/mock").MockRequest, 13 | Basic = require("jack/auth/basic/handler"); 14 | 15 | var myRealm = 'WallysWorld'; 16 | var myAuth = function(credentials) { 17 | return ('Boss' == credentials.username); 18 | } 19 | 20 | var openApp = function(env) { 21 | return { 22 | status: 200, 23 | headers: {'Content-Type': 'text/plain'}, 24 | body: ["Hi " + env['REMOTE_USER']] 25 | }; 26 | } 27 | 28 | var basicApp = Basic.Middleware(openApp, { 29 | realm: myRealm, 30 | isValid: myAuth 31 | }); 32 | 33 | var doRequest = function(request, headers) { 34 | if (headers === undefined) headers = {}; 35 | return request.GET('/', headers); 36 | } 37 | 38 | var doRequestWithBasicAuth = function(request, username, password) { 39 | return doRequest(request, {'HTTP_AUTHORIZATION': 'Basic ' + base64.encode([username, password].join(':'))}); 40 | } 41 | 42 | var doRequestWithCustomAuth = function(request, username, password) { 43 | return doRequest(request, {'HTTP_AUTHORIZATION': 'Custom ' + base64.encode([username, password].join(':'))}); 44 | } 45 | 46 | function assertBasicAuthChallenge(response) { 47 | assert.eq(401, response.status); 48 | assert.eq('text/plain', response.headers['Content-Type']); 49 | assert.isTrue(response.headers['WWW-Authenticate'].search(/^Basic/) != -1); 50 | assert.eq('Basic realm='+myRealm, response.headers['WWW-Authenticate']); 51 | } 52 | 53 | /******************************************************** 54 | * test BasicRequest 55 | ********************************************************/ 56 | 57 | exports.testBasicRequest = function() { 58 | var username = 'username', password = 'password'; 59 | var env = MockRequest.envFor(null, "/", {'HTTP_AUTHORIZATION': 'Basic ' + base64.encode([username, password].join(':'))}); 60 | var req = new Basic.Request(env); 61 | 62 | assert.isTrue(req.isBasic()); 63 | assert.eq(username, req.username); 64 | assert.eq(password, req.password); 65 | } 66 | 67 | /******************************************************** 68 | * test BasicHandler 69 | ********************************************************/ 70 | 71 | exports.testBasicHandlerValidCredentials = function() { 72 | var handler = new Basic.Handler({ 73 | realm: myRealm, 74 | isValid: myAuth 75 | }); 76 | 77 | //test handler.issueChallenge 78 | assert.eq('Basic realm='+myRealm, handler.issueChallenge()); 79 | 80 | //test handler.isValid == true 81 | var base64Credentials = base64.encode(['Boss', 'password'].join(':')); 82 | var env = MockRequest.envFor(null, "/", {'HTTP_AUTHORIZATION': 'Basic ' + base64Credentials}); 83 | var req = new Basic.Request(env); 84 | 85 | assert.isTrue(handler.isValid(req)); 86 | } 87 | 88 | exports.testBasicHandlerInvalidCredentials = function() { 89 | var handler = new Basic.Handler({ 90 | realm: myRealm, 91 | isValid: myAuth 92 | }); 93 | 94 | //test handler.isValid == false 95 | var base64Credentials = base64.encode(['username', 'password'].join(':')); 96 | var env = MockRequest.envFor(null, "/", {'HTTP_AUTHORIZATION': 'Basic ' + base64Credentials}); 97 | var req = new Basic.Request(env); 98 | 99 | assert.isFalse(handler.isValid(req)); 100 | } 101 | 102 | /******************************************************** 103 | * test Basic Auth as Jack middleware 104 | ********************************************************/ 105 | 106 | // should challenge correctly when no credentials are specified 107 | exports.testChallengeWhenNoCredentials = function() { 108 | var request = new MockRequest(basicApp); 109 | assertBasicAuthChallenge(doRequest(request)); 110 | } 111 | 112 | // should challenge correctly when incorrect credentials are specified 113 | exports.testChallengeWhenIncorrectCredentials = function() { 114 | var request = new MockRequest(basicApp); 115 | assertBasicAuthChallenge(doRequestWithBasicAuth(request, 'joe', 'password')); 116 | } 117 | 118 | // should return application output if correct credentials are specified 119 | exports.testAcceptCorrectCredentials = function() { 120 | var request = new MockRequest(basicApp); 121 | var response = doRequestWithBasicAuth(request, 'Boss', 'password'); 122 | 123 | assert.eq(200, response.status); 124 | assert.eq('Hi Boss', response.body); 125 | } 126 | 127 | // should return 400 Bad Request if different auth scheme used 128 | exports.testBadRequestIfSchemeNotBasic = function() { 129 | var request = new MockRequest(basicApp); 130 | var response = doRequestWithCustomAuth(request, 'Boss', 'password'); 131 | 132 | assert.eq(400, response.status); 133 | assert.eq(undefined, response.headers['WWW-Authenticate']); 134 | } -------------------------------------------------------------------------------- /lib/jackup.js: -------------------------------------------------------------------------------- 1 | var File = require("file"); 2 | 3 | var parser = new (require("args").Parser)(); 4 | 5 | parser.usage(' [jackup config]'); 6 | parser.help('Runs the Jackup tool to start a JSGI compatible application using a Jack handler.'); 7 | 8 | parser.option("-e", "--eval", "code") 9 | .help("evaluate a LINE of code to be used as the app (overrides module)") 10 | .set(); 11 | 12 | parser.option("-I", "--include", "lib") 13 | .help("add a library path to loader in the position of highest precedence") 14 | .action(function() { print("WARNING: -I --include not implemented"); }); 15 | 16 | parser.option("-a", "--app", "app") 17 | .help("name of the module property to use as the app (default: app)") 18 | .def("app") 19 | .set(); 20 | 21 | parser.option("-s", "--server", "server") 22 | .help("serve using SERVER") 23 | .set(); 24 | 25 | parser.option("-o", "--host", "host") 26 | .help("listen on HOST (default: 0.0.0.0)") 27 | .def("0.0.0.0") 28 | .set(); 29 | 30 | parser.option("-p", "--port", "port") 31 | .help("use PORT (default: 8080)") 32 | .def(8080) 33 | .natural(); 34 | 35 | parser.option("-E", "--env", "environment") 36 | .help("use ENVIRONMENT: deployment, development (default), none") 37 | .def("development") 38 | .set(); 39 | 40 | parser.option("-r", "--reload", "reload") 41 | .help("reload application on each request") 42 | .set(true); 43 | 44 | parser.option("-V", "--version") 45 | .help("print Jackup version number and exit.") 46 | .action(function () { 47 | this.print("Jackup Version 0.3"); 48 | this.exit(); 49 | }); 50 | 51 | parser.option("-h", "--help") 52 | .action(parser.printHelp); 53 | 54 | exports.main = function main(args) { 55 | var options = parser.parse(args); 56 | 57 | if (options.args.length > 1) { 58 | parser.printHelp(options); 59 | parser.exit(options); 60 | } 61 | exports.start(options); 62 | }; 63 | 64 | exports.start = function(options) { 65 | var config = options.args[0]; 66 | 67 | var app, configModule; 68 | 69 | if (!options.code) { 70 | if (!config) 71 | config = "jackconfig"; 72 | 73 | if (config.charAt(0) !== "/" && !config.match(/^\w:[\\\/]/)) 74 | config = File.join(File.cwd(), config); 75 | 76 | //print("Loading configuration module at " + config); 77 | 78 | system.args[0] = config; 79 | configModule = require(config); 80 | 81 | if (!configModule) 82 | throw new Error("configuration " + config + " not found"); 83 | 84 | if (options.reload) { 85 | print("Module reloading is turned on for development. Ensure that it is turned off for deployed applications for better performance"); 86 | app = require("jack/reloader").Reloader(config, options.app); 87 | } 88 | else 89 | app = configModule[options.app]; 90 | } 91 | else 92 | app = system.evalGlobal(options.code); 93 | 94 | if (typeof app !== "function") 95 | throw new Error("JSGI application must be a function, is: " + app); 96 | 97 | if (configModule && typeof configModule[options.environment] === "function") 98 | { 99 | app = configModule[options.environment](app, options); 100 | } 101 | else 102 | { 103 | switch (options.environment) { 104 | case "development" : 105 | app = require("jack/commonlogger").CommonLogger( 106 | require("jack/showexceptions").ShowExceptions(app)); 107 | break; 108 | case "deployment" : 109 | app = require("jack/commonlogger").CommonLogger(app); 110 | break; 111 | case "none" : 112 | //app = app; 113 | break; 114 | default : 115 | throw new Error("Unknown environment (development, deployment, none)"); 116 | } 117 | } 118 | 119 | if (typeof app !== "function") 120 | throw new Error("JSGI application must be a function, is: " + app); 121 | 122 | var server = options.server; 123 | if (!server) 124 | { 125 | if (system.env["PHP_FCGI_CHILDREN"] !== undefined) 126 | server = "fastcgi"; 127 | else if (system.engine === "rhino") 128 | server = "simple"; 129 | else if (system.engine === "k7") 130 | server = "shttpd"; 131 | else if (system.engine === "v8cgi") 132 | server = "v8cgi"; 133 | else 134 | throw new Error("Unknown engine " + system.engine + ". Specify a server with the \"-s\" option."); 135 | } 136 | // Load the required handler. 137 | var handler = null; 138 | try { 139 | handler = require("jack/handler/" + server); 140 | } catch (e) { 141 | throw new Error("Jack handler \""+server+"\" not found "); 142 | } 143 | 144 | if (handler && typeof handler.run !== "function") 145 | throw new Error("Jack handler must be a function, is: " + handler.run); 146 | 147 | return handler.run(app, options); 148 | }; 149 | 150 | if (module.id == require.main) 151 | exports.main(system.args); 152 | 153 | -------------------------------------------------------------------------------- /docs/jsgi-spec.md: -------------------------------------------------------------------------------- 1 | 2 | JSGI Specification, v0.2 3 | ======================== 4 | 5 | Applications 6 | ------------ 7 | 8 | A JSGI application is a JavaScript function. It takes exactly one argument, the **environment**, and returns a JavaScript Object containing three properties: the **status**, the **headers**, and the **body**. 9 | 10 | Middleware 11 | ---------- 12 | 13 | JSGI middleware is typically a function that takes at least one other JSGI application and returns another function which is also a JSGI application. 14 | 15 | The Environment 16 | --------------- 17 | 18 | The environment must be a JavaScript Object instance that includes CGI-like headers. The application is free to modify the environment. The environment is required to include these variables (adopted from PEP333 and Rack), except when they'd be empty, but see below. 19 | 20 | * `REQUEST_METHOD`: The HTTP request method, such as "GET" or "POST". This cannot ever be an empty string, and so is always required. 21 | * `SCRIPT_NAME`: The initial portion of the request URL‘s "path" that corresponds to the application object, so that the application knows its virtual "location". This may be an empty string, if the application corresponds to the "root" of the server. 22 | * `PATH_INFO`: The remainder of the request URL‘s "path", designating the virtual "location" of the request‘s target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when I originating from a URL. 23 | * `QUERY_STRING`: The portion of the request URL that follows the ?, if any. May be empty, but is always required! 24 | * `SERVER_NAME`, `SERVER_PORT`: When combined with `SCRIPT_NAME` and `PATH_INFO`, these variables can be used to complete the URL. Note, however, that `HTTP_HOST`, if present, should be used in preference to `SERVER_NAME` for reconstructing the request URL. `SERVER_NAME` and `SERVER_PORT` can never be empty strings, and so are always required. 25 | * HTTP_ Variables: Variables corresponding to the client-supplied HTTP request headers (i.e., variables whose names begin with HTTP\_). The presence or absence of these variables should correspond with the presence or absence of the appropriate HTTP header in the request. 26 | 27 | In addition to this, the JSGI environment must include these JSGI-specific variables: 28 | 29 | * `jsgi.version`: The Array \[0,2\], representing this version of JSGI. 30 | * `jsgi.url_scheme`: http or https, depending on the request URL. 31 | * `jsgi.input`: See below, the input stream. 32 | * `jsgi.errors`: See below, the error stream. 33 | * `jsgi.multithread`: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise. 34 | * `jsgi.multiprocess`: true if an equivalent application object may be simultaneously invoked by another process, false otherwise. 35 | * `jsgi.run_once`: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar). 36 | 37 | The server or the application can store their own data in the environment, too. The keys must contain at least one dot, and should be prefixed uniquely. The prefix *jsgi.* is reserved for use with the JSGI core distribution and must not be used otherwise. The environment must not contain the keys `HTTP_CONTENT_TYPE` or `HTTP_CONTENT_LENGTH` (use the versions without HTTP_). The CGI keys (named without a period) must have String values. There are the following restrictions: 38 | 39 | * `jsgi.version` must be an array of Integers. 40 | * `jsgi.url_scheme` must either be http or https. 41 | * There must be a valid input stream in `jsgi.input`. 42 | * There must be a valid error stream in `jsgi.errors`. 43 | * The `REQUEST_METHOD` must be a valid token. 44 | * The `SCRIPT_NAME`, if non-empty, must start with / 45 | * The `PATH_INFO`, if non-empty, must start with / 46 | * The `CONTENT_LENGTH`, if given, must consist of digits only. 47 | * One of `SCRIPT_NAME` or `PATH_INFO` must be set. `PATH_INFO` should be / if `SCRIPT_NAME` is empty. `SCRIPT_NAME` never should be /, but instead be empty. 48 | 49 | ### The Input Stream 50 | 51 | Must be an input stream. 52 | 53 | ### The Error Stream 54 | 55 | Must be an output stream. 56 | 57 | 58 | The Response 59 | ------------ 60 | 61 | ### The Status 62 | 63 | The status, if parsed as integer, must be greater than or equal to 100. 64 | 65 | ### The Headers 66 | 67 | The header must be a JavaScript object containing key/value pairs of Strings. The header must not contain a Status key, contain keys with : or newlines in their name, contain keys names that end in - or \_, but only contain keys that consist of letters, digits, \_ or - and start with a letter. The values of the header must be Strings, consisting of lines (for multiple header values) separated by "\n". The lines must not contain characters below 037. 68 | 69 | ### The Content-Type 70 | 71 | There must be a `Content-Type`, except when the Status is 1xx, 204 or 304, in which case there must be none given. 72 | 73 | ### The Content-Length 74 | 75 | There must not be a Content-Length header when the Status is 1xx, 204 or 304. 76 | 77 | ### The Body 78 | 79 | The Body must respond to `forEach` and must only yield objects which have a `toByteString` method (including Strings and Binary objects). If the Body responds to `close`, it will be called after iteration. The Body commonly is an array of Strings or ByteStrings. 80 | 81 | 82 | Acknowledgements 83 | ---------------- 84 | 85 | This specification is adapted from the Rack specification ([http://rack.rubyforge.org/doc/files/SPEC.html](http://rack.rubyforge.org/doc/files/SPEC.html)) written by Christian Neukirchen. 86 | 87 | Some parts of this specification are adopted from PEP333: Python Web Server Gateway Interface v1.0 ([www.python.org/dev/peps/pep-0333/](www.python.org/dev/peps/pep-0333/)). 88 | -------------------------------------------------------------------------------- /lib/jack/response.js: -------------------------------------------------------------------------------- 1 | var HashP = require("hashp").HashP; 2 | 3 | var Response = exports.Response = function(status, headers, body) { 4 | var that = this; 5 | 6 | if (typeof arguments[0] === "object") { 7 | headers = arguments[0].headers; 8 | body = arguments[0].body; 9 | status = arguments[0].status; 10 | } 11 | 12 | this.status = status || 200; 13 | if (this.status !== 304) { 14 | this.headers = HashP.merge({"Content-Type" : "text/html"}, headers); 15 | } else { 16 | this.headers = headers || {}; 17 | } 18 | 19 | this.body = []; 20 | this.length = 0; 21 | this.writer = function(bytes) { that.body.push(bytes); }; 22 | 23 | this.block = null; 24 | 25 | if (body) 26 | { 27 | if (typeof body.forEach === "function") 28 | { 29 | body.forEach(function(part) { 30 | that.write(part); 31 | }); 32 | } 33 | else 34 | throw new Error("iterable required"); 35 | } 36 | } 37 | 38 | Response.prototype.setHeader = function(key, value) { 39 | HashP.set(this.headers, key, value); 40 | } 41 | 42 | Response.prototype.addHeader = function(key, value) { 43 | var header = HashP.get(this.headers, key); 44 | 45 | if (!header) 46 | HashP.set(this.headers, key, value); 47 | else if (typeof header === "string") 48 | HashP.set(this.headers, key, [header, value]); 49 | else // Array 50 | header.push(value); 51 | } 52 | 53 | Response.prototype.getHeader = function(key) { 54 | return HashP.get(this.headers, key); 55 | } 56 | 57 | Response.prototype.unsetHeader = function(key) { 58 | return HashP.unset(this.headers, key); 59 | } 60 | 61 | Response.prototype.setCookie = function(key, value) { 62 | var domain, path, expires, secure, httponly; 63 | 64 | var cookie = encodeURIComponent(key) + "=", 65 | meta = ""; 66 | 67 | if (typeof value === "object") { 68 | if (value.domain) meta += "; domain=" + value.domain ; 69 | if (value.path) meta += "; path=" + value.path; 70 | if (value.expires) meta += "; expires=" + value.expires.toGMTString(); 71 | if (value.secure) meta += "; secure"; 72 | if (value.httpOnly) meta += "; HttpOnly"; 73 | value = value.value; 74 | } 75 | 76 | if (Array.isArray(value)) { 77 | for (var i = 0; i < value.length; i++) 78 | cookie += encodeURIComponent(value[i]); 79 | } else { 80 | cookie += encodeURIComponent(value); 81 | } 82 | 83 | cookie = cookie + meta; 84 | 85 | this.addHeader("Set-Cookie", cookie); 86 | } 87 | 88 | Response.prototype.deleteCookie = function(key) { 89 | this.setCookie(key, { expires: 0 }); 90 | } 91 | 92 | Response.prototype.redirect = function(location, status) { 93 | this.status = status || 302; 94 | this.addHeader("Location", location); 95 | this.write('Go to ' + location + ""); 96 | } 97 | 98 | Response.prototype.write = function(object) { 99 | var binary = object.toByteString('utf-8'); 100 | this.writer(binary); 101 | this.length += binary.length; 102 | 103 | // TODO: or 104 | // this.writer(binary); 105 | // this.length += binary.byteLength(); 106 | 107 | HashP.set(this.headers, "Content-Length", this.length.toString(10)); 108 | } 109 | 110 | Response.prototype.finish = function(block) { 111 | this.block = block; 112 | 113 | if (this.status == 204 || this.status == 304) 114 | { 115 | HashP.unset(this.headers, "Content-Type"); 116 | return { 117 | status : this.status, 118 | headers : this.headers, 119 | body : [] 120 | }; 121 | } 122 | else 123 | { 124 | return { 125 | status : this.status, 126 | headers : this.headers, 127 | body : this 128 | }; 129 | } 130 | } 131 | 132 | Response.prototype.forEach = function(callback) { 133 | this.body.forEach(callback); 134 | 135 | this.writer = callback; 136 | if (this.block) 137 | this.block(this); 138 | } 139 | 140 | Response.prototype.close = function() { 141 | if (this.body.close) 142 | this.body.close(); 143 | } 144 | 145 | Response.prototype.isEmpty = function() { 146 | return !this.block && this.body.length === 0; 147 | } 148 | 149 | exports.redirect = Response.redirect = function(location, status) { 150 | status = status || 303; 151 | var body = 'Go to ' + location + ''; 152 | return { 153 | status : status, 154 | headers : { 155 | "Location": location, 156 | "Content-Type": "text/plain", 157 | "Content-Length": String(body.length) 158 | }, 159 | body : [body] 160 | }; 161 | } 162 | 163 | var AsyncResponse = exports.AsyncResponse = function(status, headers, body) { 164 | // set the buffer up first, since Response's constructor calls .write() 165 | this._buffer = []; 166 | 167 | this._callback = null; 168 | this._errback = null; 169 | 170 | Response.apply(this, arguments); 171 | 172 | this.body = { forEach : this.forEach.bind(this) }; 173 | } 174 | 175 | AsyncResponse.prototype = Object.create(Response.prototype); 176 | 177 | // this "write" gets overriden later by the callback provided to forEach 178 | AsyncResponse.prototype.write = function(chunk) { 179 | this._buffer.push(chunk); 180 | } 181 | 182 | AsyncResponse.prototype.forEach = function(callback) { 183 | this._buffer.forEach(callback); 184 | this._buffer = null; 185 | 186 | this.write = callback; 187 | 188 | return { then : this.then.bind(this) }; 189 | } 190 | 191 | AsyncResponse.prototype.then = function(callback, errback) { 192 | this._callback = callback; 193 | this._errback = errback; 194 | } 195 | 196 | AsyncResponse.prototype.close = function() { 197 | this._callback(); 198 | } 199 | -------------------------------------------------------------------------------- /lib/jack/handler/mozhttpd.js: -------------------------------------------------------------------------------- 1 | const CC = Components.Constructor; 2 | const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", "nsIBinaryOutputStream", "setOutputStream"); 3 | const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream"); 4 | var Log = new (require("logger")).Logger({write: print}); 5 | Log.level = 4; 6 | Log.trace = function(object) { print(require("test/jsdump").jsDump.parse(object)) } 7 | 8 | // handler for Simple (http://simpleweb.sourceforge.net/) based on the servlet handler 9 | var IO = require("io").IO, 10 | fs = require("file"), 11 | HashP = require("hashp").HashP, 12 | URI = require("uri"), 13 | HTTP_STATUS_CODES = require("../utils").HTTP_STATUS_CODES; 14 | 15 | exports.run = function(app, options) { 16 | var options = options || {}; 17 | var server = new (require("mozhttpd-engine")).Server(); 18 | // overriding default handler 19 | server._handler._handleDefault = function(request, response) { 20 | try { 21 | process(app, request, response); 22 | } catch(e) { 23 | Log.error(e); 24 | Log.error(e.stack); 25 | throw e; 26 | } 27 | }; 28 | 29 | // different version 30 | var port = options["port"] || 8080, 31 | address = options["host"] || "localhost"; 32 | Log.debug("starting server on port:", port); 33 | server.start(port); 34 | } 35 | 36 | var process = function(app, request, response) { 37 | Log.debug("request received"); 38 | var env = {}; 39 | 40 | // Log.trace(request); 41 | // copy HTTP headers over, converting where appropriate 42 | var requestHeaders = request.headers; 43 | while (requestHeaders.hasMoreElements()) { 44 | key = new String(requestHeaders.getNext().QueryInterface(Components.interfaces.nsISupportsString)); 45 | value = request.getHeader(key); 46 | key = key.replace(/-/g, "_").toUpperCase(); 47 | if (!key.match(/(CONTENT_TYPE|CONTENT_LENGTH)/i)) key = "HTTP_" + key; 48 | env[key] = value; 49 | } 50 | Log.debug("headers had bein copied"); 51 | 52 | //var address = request.getAddress(); 53 | 54 | if (env["HTTP_HOST"]) { 55 | var parts = env["HTTP_HOST"].split(":"); 56 | if (parts.length === 2) 57 | { 58 | env["SERVER_NAME"] = parts[0]; 59 | env["SERVER_PORT"] = parts[1]; 60 | } 61 | } 62 | 63 | var uri = URI.parse(request.target); 64 | 65 | env["SERVER_NAME"] = env["SERVER_NAME"] || "mozhttpd"; 66 | env["SERVER_PORT"] = env["SERVER_PORT"] || request.port; 67 | 68 | env["SCRIPT_NAME"] = ""; 69 | env["PATH_INFO"] = uri.path || request.path || ""; 70 | 71 | env["REQUEST_METHOD"] = request.method || ""; 72 | env["QUERY_STRING"] = uri.query || request.queryString || ""; 73 | env["SERVER_PROTOCOL"] = ["HTTP/", request.httpVersion || "1.1"].join(""); 74 | env["HTTP_VERSION"] = env["SERVER_PROTOCOL"]; // legacy 75 | 76 | var cAddr, addr; 77 | //if (cAddr = request.getClientAddress()) 78 | // env["REMOTE_ADDR"] = String(cAddr.getHostName() || cAddr.getAddress() || ""); 79 | 80 | env["jsgi.version"] = [0,2]; 81 | env["jsgi.input"] = new IO(new BinaryInputStream(request.bodyInputStream), null); 82 | env["jsgi.errors"] = { 83 | print: system.print, 84 | flush: function() {}, 85 | write: function() { 86 | dump(Array.prototype.join.call(arguments, " ")); 87 | } 88 | }; //system.stderr; 89 | env["jsgi.multithread"] = true; 90 | env["jsgi.multiprocess"] = false; 91 | env["jsgi.run_once"] = false; 92 | env["jsgi.url_scheme"] = request.scheme || uri.scheme || "http"; 93 | 94 | // efficiently serve files if the server supports it 95 | env["HTTP_X_ALLOW_SENDFILE"] = "yes"; 96 | 97 | for (var key in env) 98 | Log.debug(key, ":", env[key]) 99 | // call the app 100 | var res = app(env); 101 | 102 | Log.debug("response has been processed by app", res); 103 | 104 | Object.keys(response).forEach(function(key) { 105 | print(key + " : " + response[key]); 106 | }); 107 | // set the status 108 | Log.debug("set the status", env["SERVER_PROTOCOL"], res.status, HTTP_STATUS_CODES[res.status]); 109 | response.setStatusLine(request.httpVersion || "1.1", res.status, HTTP_STATUS_CODES[res.status]); 110 | 111 | // check to see if X-Sendfile was used, remove the header 112 | var sendfilePath = null; 113 | if (HashP.includes(res.headers, "X-Sendfile")) { 114 | sendfilePath = HashP.unset(res.headers, "X-Sendfile"); 115 | Log.debug("check to see if X-Sendfile was used, remove the header", sendfilePath); 116 | HashP.set(res.headers, "Content-Length", new String(fs.size(sendfilePath))); 117 | } 118 | 119 | // set the headers 120 | for (var key in res.headers) { 121 | Log.debug(">> set header", key, res.headers[key]); 122 | try { 123 | response.setHeader(key, res.headers[key], false); 124 | } catch(e) { 125 | Log.error(e) 126 | } 127 | } 128 | Log.debug("set the headers"); 129 | 130 | // determine if the response should be chunked (FIXME: need a better way?) 131 | var chunked = HashP.includes(res.headers, "Transfer-Encoding") && HashP.get(res.headers, "Transfer-Encoding") !== 'identity'; 132 | 133 | 134 | // X-Sendfile send 135 | if (sendfilePath) { 136 | fs.FileIO(sendfilePath, "r").copy(response.bodyOutputStream); 137 | response.bodyOutputStream.flush(); 138 | } 139 | 140 | var output = new IO(null, new BinaryOutputStream(response.bodyOutputStream)); 141 | // output the body, flushing after each write if it's chunked 142 | res.body.forEach(function(chunk) { 143 | if (!sendfilePath) { 144 | //output.write(new java.lang.String(chunk).getBytes("US-ASCII")); 145 | //output.write(chunk, "US-ASCII"); 146 | output.write(chunk); 147 | if (chunked) output.flush(); 148 | } 149 | }); 150 | //output.close(); 151 | } -------------------------------------------------------------------------------- /lib/jack/mime.js: -------------------------------------------------------------------------------- 1 | 2 | // returns MIME type for extension, or fallback, or octet-steam 3 | exports.mimeType = function(ext, fallback) { 4 | return exports.MIME_TYPES[ext.toLowerCase()] || fallback || 'application/octet-stream'; 5 | }, 6 | 7 | // List of most common mime-types, stolen from Rack. 8 | exports.MIME_TYPES = { 9 | ".3gp" : "video/3gpp", 10 | ".a" : "application/octet-stream", 11 | ".ai" : "application/postscript", 12 | ".aif" : "audio/x-aiff", 13 | ".aiff" : "audio/x-aiff", 14 | ".asc" : "application/pgp-signature", 15 | ".asf" : "video/x-ms-asf", 16 | ".asm" : "text/x-asm", 17 | ".asx" : "video/x-ms-asf", 18 | ".atom" : "application/atom+xml", 19 | ".au" : "audio/basic", 20 | ".avi" : "video/x-msvideo", 21 | ".bat" : "application/x-msdownload", 22 | ".bin" : "application/octet-stream", 23 | ".bmp" : "image/bmp", 24 | ".bz2" : "application/x-bzip2", 25 | ".c" : "text/x-c", 26 | ".cab" : "application/vnd.ms-cab-compressed", 27 | ".cc" : "text/x-c", 28 | ".chm" : "application/vnd.ms-htmlhelp", 29 | ".class" : "application/octet-stream", 30 | ".com" : "application/x-msdownload", 31 | ".conf" : "text/plain", 32 | ".cpp" : "text/x-c", 33 | ".crt" : "application/x-x509-ca-cert", 34 | ".css" : "text/css", 35 | ".csv" : "text/csv", 36 | ".cxx" : "text/x-c", 37 | ".deb" : "application/x-debian-package", 38 | ".der" : "application/x-x509-ca-cert", 39 | ".diff" : "text/x-diff", 40 | ".djv" : "image/vnd.djvu", 41 | ".djvu" : "image/vnd.djvu", 42 | ".dll" : "application/x-msdownload", 43 | ".dmg" : "application/octet-stream", 44 | ".doc" : "application/msword", 45 | ".dot" : "application/msword", 46 | ".dtd" : "application/xml-dtd", 47 | ".dvi" : "application/x-dvi", 48 | ".ear" : "application/java-archive", 49 | ".eml" : "message/rfc822", 50 | ".eps" : "application/postscript", 51 | ".exe" : "application/x-msdownload", 52 | ".f" : "text/x-fortran", 53 | ".f77" : "text/x-fortran", 54 | ".f90" : "text/x-fortran", 55 | ".flv" : "video/x-flv", 56 | ".for" : "text/x-fortran", 57 | ".gem" : "application/octet-stream", 58 | ".gemspec" : "text/x-script.ruby", 59 | ".gif" : "image/gif", 60 | ".gz" : "application/x-gzip", 61 | ".h" : "text/x-c", 62 | ".hh" : "text/x-c", 63 | ".htm" : "text/html", 64 | ".html" : "text/html", 65 | ".ico" : "image/vnd.microsoft.icon", 66 | ".ics" : "text/calendar", 67 | ".ifb" : "text/calendar", 68 | ".iso" : "application/octet-stream", 69 | ".jar" : "application/java-archive", 70 | ".java" : "text/x-java-source", 71 | ".jnlp" : "application/x-java-jnlp-file", 72 | ".jpeg" : "image/jpeg", 73 | ".jpg" : "image/jpeg", 74 | ".js" : "application/javascript", 75 | ".json" : "application/json", 76 | ".log" : "text/plain", 77 | ".m3u" : "audio/x-mpegurl", 78 | ".m4v" : "video/mp4", 79 | ".man" : "text/troff", 80 | ".mathml" : "application/mathml+xml", 81 | ".mbox" : "application/mbox", 82 | ".mdoc" : "text/troff", 83 | ".me" : "text/troff", 84 | ".mid" : "audio/midi", 85 | ".midi" : "audio/midi", 86 | ".mime" : "message/rfc822", 87 | ".mml" : "application/mathml+xml", 88 | ".mng" : "video/x-mng", 89 | ".mov" : "video/quicktime", 90 | ".mp3" : "audio/mpeg", 91 | ".mp4" : "video/mp4", 92 | ".mp4v" : "video/mp4", 93 | ".mpeg" : "video/mpeg", 94 | ".mpg" : "video/mpeg", 95 | ".ms" : "text/troff", 96 | ".msi" : "application/x-msdownload", 97 | ".odp" : "application/vnd.oasis.opendocument.presentation", 98 | ".ods" : "application/vnd.oasis.opendocument.spreadsheet", 99 | ".odt" : "application/vnd.oasis.opendocument.text", 100 | ".ogg" : "application/ogg", 101 | ".p" : "text/x-pascal", 102 | ".pas" : "text/x-pascal", 103 | ".pbm" : "image/x-portable-bitmap", 104 | ".pdf" : "application/pdf", 105 | ".pem" : "application/x-x509-ca-cert", 106 | ".pgm" : "image/x-portable-graymap", 107 | ".pgp" : "application/pgp-encrypted", 108 | ".pkg" : "application/octet-stream", 109 | ".pl" : "text/x-script.perl", 110 | ".pm" : "text/x-script.perl-module", 111 | ".png" : "image/png", 112 | ".pnm" : "image/x-portable-anymap", 113 | ".ppm" : "image/x-portable-pixmap", 114 | ".pps" : "application/vnd.ms-powerpoint", 115 | ".ppt" : "application/vnd.ms-powerpoint", 116 | ".ps" : "application/postscript", 117 | ".psd" : "image/vnd.adobe.photoshop", 118 | ".py" : "text/x-script.python", 119 | ".qt" : "video/quicktime", 120 | ".ra" : "audio/x-pn-realaudio", 121 | ".rake" : "text/x-script.ruby", 122 | ".ram" : "audio/x-pn-realaudio", 123 | ".rar" : "application/x-rar-compressed", 124 | ".rb" : "text/x-script.ruby", 125 | ".rdf" : "application/rdf+xml", 126 | ".roff" : "text/troff", 127 | ".rpm" : "application/x-redhat-package-manager", 128 | ".rss" : "application/rss+xml", 129 | ".rtf" : "application/rtf", 130 | ".ru" : "text/x-script.ruby", 131 | ".s" : "text/x-asm", 132 | ".sgm" : "text/sgml", 133 | ".sgml" : "text/sgml", 134 | ".sh" : "application/x-sh", 135 | ".sig" : "application/pgp-signature", 136 | ".snd" : "audio/basic", 137 | ".so" : "application/octet-stream", 138 | ".svg" : "image/svg+xml", 139 | ".svgz" : "image/svg+xml", 140 | ".swf" : "application/x-shockwave-flash", 141 | ".t" : "text/troff", 142 | ".tar" : "application/x-tar", 143 | ".tbz" : "application/x-bzip-compressed-tar", 144 | ".tcl" : "application/x-tcl", 145 | ".tex" : "application/x-tex", 146 | ".texi" : "application/x-texinfo", 147 | ".texinfo" : "application/x-texinfo", 148 | ".text" : "text/plain", 149 | ".tif" : "image/tiff", 150 | ".tiff" : "image/tiff", 151 | ".torrent" : "application/x-bittorrent", 152 | ".tr" : "text/troff", 153 | ".txt" : "text/plain", 154 | ".vcf" : "text/x-vcard", 155 | ".vcs" : "text/x-vcalendar", 156 | ".vrml" : "model/vrml", 157 | ".war" : "application/java-archive", 158 | ".wav" : "audio/x-wav", 159 | ".wma" : "audio/x-ms-wma", 160 | ".wmv" : "video/x-ms-wmv", 161 | ".wmx" : "video/x-ms-wmx", 162 | ".wrl" : "model/vrml", 163 | ".wsdl" : "application/wsdl+xml", 164 | ".xbm" : "image/x-xbitmap", 165 | ".xhtml" : "application/xhtml+xml", 166 | ".xls" : "application/vnd.ms-excel", 167 | ".xml" : "application/xml", 168 | ".xpm" : "image/x-xpixmap", 169 | ".xsl" : "application/xml", 170 | ".xslt" : "application/xslt+xml", 171 | ".yaml" : "text/yaml", 172 | ".yml" : "text/yaml", 173 | ".zip" : "application/zip" 174 | } 175 | -------------------------------------------------------------------------------- /lib/jack/handler/simple-worker.js: -------------------------------------------------------------------------------- 1 | var IO = require("io").IO, 2 | file = require("file"), 3 | when = require("promise").when, 4 | HTTP_STATUS_CODES = require("../utils").HTTP_STATUS_CODES, 5 | HashP = require("hashp").HashP, 6 | jackup = require("jackup"), 7 | ByteString = require("binary").ByteString, 8 | ByteArray = require("binary").ByteArray, 9 | parse = require("uri").parse; 10 | 11 | onconnect = function (e) { 12 | exports.httpWorker = e.port; 13 | onconnect = null; 14 | }; 15 | 16 | onstart = function(options){ 17 | exports.options = options; 18 | jackup.start(options); 19 | }; 20 | 21 | 22 | exports.run = function(app, options) { 23 | 24 | onrequest = function(request, response) { 25 | var req = {headers:{}}; 26 | 27 | // copy HTTP headers over, converting where appropriate 28 | for (var e = request.getNames().iterator(); e.hasNext();) 29 | { 30 | var name = String(e.next()), 31 | value = String(request.getValue(name)), // FIXME: only gets the first of multiple 32 | key = name.toLowerCase(); 33 | 34 | req.headers[key] = value; 35 | } 36 | 37 | var address = request.getAddress(); 38 | 39 | if (req.headers.host) 40 | { 41 | var parts = req.headers.host.split(":"); 42 | if (parts.length === 2) 43 | { 44 | req.host = parts[0]; 45 | req.port = parts[1]; 46 | } 47 | } 48 | 49 | var uri = parse(String(request.getTarget())); 50 | 51 | req.env = {}; // don't know what actually goes in here 52 | var name = address.getDomain(), 53 | port = address.getPort(); 54 | req.serverName = request.serverName || String(name || ""); 55 | req.serverPort = request.serverPort || String(port >= 0 ? port : ""); 56 | 57 | req.scriptName = ""; 58 | req.pathInfo = uri.path || ""; 59 | 60 | req.method = String(request.getMethod() || ""); 61 | req.queryString = uri.query || ""; 62 | req.version = "HTTP/"+request.getMajor()+"."+request.getMinor(); 63 | 64 | var cAddr, addr; 65 | if (cAddr = request.getClientAddress()) 66 | req.remoteHost = String(cAddr.getHostName() || cAddr.getAddress() || ""); 67 | 68 | req.body = new IO(request.getInputStream(), null); 69 | 70 | req.body.forEach = function(callback) { 71 | var readLength, 72 | buffer = new ByteArray(4096); 73 | 74 | while((readLength = IO.prototype.readInto.call(this, buffer, 4096, 0)) > 0){ 75 | var data = new ByteString(buffer._bytes, 0, readLength); 76 | callback(data.decodeToString(this.encoding || "UTF-8")); 77 | } 78 | }; 79 | var is; 80 | function read(length){ 81 | if(!is){ 82 | is = request.getInputStream(); 83 | } 84 | 85 | if(!length){ 86 | length = 4096; 87 | } 88 | var buffer = Packages.java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, length); 89 | var bytesRead = is.read(buffer); 90 | return { 91 | length: bytesRead, 92 | bytes: buffer 93 | } 94 | } 95 | req.jsgi={version: [0, 3], 96 | errors: system.stderr, 97 | multithread: false, 98 | multiprocess: true, 99 | ext: { async: [0, 1] }, 100 | runOnce: false}; 101 | req.scheme = String(address.getScheme() || "http"); 102 | var cAddr; 103 | if (cAddr = request.getClientAddress()) 104 | req.remoteAddr = String(cAddr.getHostName() || cAddr.getAddress() || ""); 105 | 106 | // efficiently serve files if the server supports it 107 | req["x-sendfile"] = "yes"; 108 | // call the app 109 | var output, responseStarted = false; 110 | try{ 111 | var res = app(req); 112 | 113 | // use the promise manager to determine when the app is done 114 | // in a normal sync request, it will just execute the fulfill 115 | // immediately 116 | when(res, function(res){ 117 | // success handler 118 | 119 | try{ 120 | handleResponse(res); 121 | } 122 | catch(e){ 123 | print(String((e.rhinoException && e.rhinoException.printStackTrace()) || (e.name + ": " + e.message))); 124 | response.getOutputStream().write(e); 125 | response.getOutputStream().close(); 126 | } 127 | }, onError); 128 | }catch(e){ 129 | onError(e); 130 | } 131 | function onError(error){ 132 | // unhandled error 133 | try{ 134 | handleResponse({status:500, headers:{}, body:[error.message]}); 135 | }catch(e){ 136 | print(String((error.rhinoException && error.rhinoException.printStackTrace()) || (error.name + ": " + error.message))); 137 | } 138 | } 139 | function handleResponse(res){ 140 | // set the status 141 | response.setCode(res.status); 142 | response.setText(HTTP_STATUS_CODES[res.status]); 143 | 144 | // check to see if X-Sendfile was used, remove the header 145 | var sendfilePath = null; 146 | if (HashP.includes(res.headers, "X-Sendfile")) { 147 | sendfilePath = HashP.unset(res.headers, "X-Sendfile"); 148 | HashP.set(res.headers, "Content-Length", String(file.size(sendfilePath))); 149 | } 150 | 151 | // set the headers 152 | for (var key in res.headers) { 153 | var headerValue = res.headers[key]; 154 | ('' + headerValue).split("\n").forEach(function(value) { 155 | response.add(key, value); 156 | }); 157 | } 158 | 159 | // determine if the response should be chunked (FIXME: need a better way?) 160 | var chunked = HashP.includes(res.headers, "Transfer-Encoding") && HashP.get(res.headers, "Transfer-Encoding") !== 'identity'; 161 | 162 | output = new IO(null, response.getOutputStream()); 163 | 164 | // X-Sendfile send 165 | if (sendfilePath) { 166 | var cIn = new java.io.FileInputStream(sendfilePath).getChannel(), 167 | cOut = response.getByteChannel(); 168 | 169 | cIn.transferTo(0, cIn.size(), cOut); 170 | 171 | cIn.close(); 172 | cOut.close(); 173 | return; 174 | } 175 | try{ 176 | // output the body, flushing after each write if it's chunked 177 | var possiblePromise = res.body.forEach(function(chunk) { 178 | if (!sendfilePath) { 179 | //output.write(new java.lang.String(chunk).getBytes("US-ASCII")); 180 | //output.write(chunk, "US-ASCII"); 181 | // with async/promises, it would actually be more ideal to set the headers 182 | // and status here before the first write, so that a promise could suspend 183 | // and then set status when it resumed 184 | output.write(chunk); 185 | 186 | if (chunked) 187 | output.flush(); 188 | } 189 | }); 190 | 191 | // check to see if the return value is a promise 192 | // alternately we could check with if (possiblePromise instanceof require("promise").Promise) 193 | if (possiblePromise && typeof possiblePromise.then == "function") { 194 | // its a promise, don't close the output until it is fulfilled 195 | possiblePromise.then(function() { 196 | // fulfilled, we are done now 197 | output.close(); 198 | }, 199 | function(e) { 200 | // an error, just write the error and finish 201 | output.write(e.message); 202 | output.close(); 203 | }); 204 | } 205 | else { 206 | // not a promise, regular sync request, we are done now 207 | output.close(); 208 | } 209 | } 210 | catch(e){ 211 | output.write(String((e.rhinoException && e.rhinoException.printStackTrace()) || (e.name + ": " + e.message))); 212 | if(chunked){ 213 | output.flush(); 214 | } 215 | output.close(); 216 | } 217 | 218 | 219 | } 220 | 221 | 222 | } 223 | }; -------------------------------------------------------------------------------- /tests/jack/auth/auth-digest-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Neville Burnell 3 | * See http://github.com/cloudwork/jack/lib/jack/auth/README.md for license 4 | * 5 | * Acknowledgements: 6 | * Inspired by Rack::Auth 7 | * http://github.com/rack/rack 8 | */ 9 | 10 | var assert = require("test/assert"), 11 | Hash = require("hash").Hash, 12 | HashP = require("hashp").HashP, 13 | Jack = require("jack"), 14 | MockRequest = require("jack/mock").MockRequest, 15 | DigestNonce = require("jack/auth/digest/nonce"), 16 | DigestHandler = require("jack/auth/digest/handler"), 17 | DigestRequest = require("jack/auth/digest/request").DigestRequest, 18 | DigestParams = require("jack/auth/digest/params"); 19 | 20 | var myRealm = "WallysWorld"; 21 | 22 | /********************************** 23 | * apps 24 | *********************************/ 25 | 26 | var openApp = function(env) { 27 | return { 28 | status: 200, 29 | headers: {'Content-Type': 'text/plain'}, 30 | body: ["Hi " + env['REMOTE_USER']] 31 | }; 32 | } 33 | 34 | var digestApp = DigestHandler.Middleware(openApp, { 35 | realm: myRealm, 36 | opaque: "this-should-be-secret", 37 | getPassword: function(username) { 38 | return {'Alice': 'correct-password'}[username]; 39 | } 40 | }); 41 | 42 | var partiallyProtectedApp = Jack.URLMap({ 43 | '/': openApp, 44 | '/protected': digestApp 45 | }); 46 | 47 | /********************************** 48 | * helpers 49 | *********************************/ 50 | 51 | var doRequest = function(request, method, path, headers) { 52 | return request[method](path, headers || {}); 53 | } 54 | 55 | 56 | var doRequestWithDigestAuth = function(request, method, path, username, password, options) { 57 | if (!options) options = {}; 58 | 59 | var headers = {}; 60 | 61 | if (options.input) { 62 | headers.input = options.input; 63 | delete options.input; 64 | } 65 | 66 | var response = doRequest(request, method, path, headers); 67 | if (response.status != 401) return response; 68 | 69 | if (options.wait) { 70 | var sleep = require('os-engine').sleep; //sleep() is exported by the rhino engine 71 | if (sleep) sleep(options.wait); 72 | delete options.wait; 73 | } 74 | 75 | var challenge = HashP.get(response.headers, 'WWW-Authenticate').match(/digest (.*)/i).pop(); 76 | 77 | var params = DigestParams.parse({ 78 | username: username, 79 | nc: '00000001', 80 | cnonce: 'nonsensenonce', 81 | uri: path, 82 | method: method 83 | }, challenge); 84 | 85 | Hash.update(params, options); 86 | 87 | params.response = DigestHandler.digest(params, password); 88 | HashP.set(headers, 'HTTP_AUTHORIZATION', "Digest "+DigestParams.toString(params)); 89 | 90 | return doRequest(request, method, path, headers); 91 | } 92 | 93 | /******************************************************** 94 | * assertions 95 | ********************************************************/ 96 | 97 | var assertDigestAuthChallenge = function(response){ 98 | assert.eq(401, response.status); 99 | assert.eq('text/plain', response.headers['Content-Type']); 100 | assert.isFalse(response.headers['WWW-Authenticate'] === undefined); 101 | assert.isTrue(response.headers['WWW-Authenticate'].search(/^Digest/) != -1); 102 | } 103 | 104 | var assertBadRequest = function(response) { 105 | assert.eq(400, response.status); 106 | assert.isTrue(response.headers['WWW-Authenticate'] === undefined); 107 | } 108 | 109 | /******************************************************** 110 | * test Basic Auth as Jack middleware 111 | ********************************************************/ 112 | 113 | // should return application output for GET when correct credentials given 114 | exports.testAcceptGetWithCorrectCredentials = function() { 115 | var request = new MockRequest(digestApp); 116 | var response = doRequestWithDigestAuth(request, 'GET', '/', 'Alice', 'correct-password'); 117 | 118 | assert.eq(200, response.status); 119 | assert.eq("Hi Alice", response.body.toString()); 120 | }; 121 | 122 | // should return application output for POST when correct credentials given 123 | exports.testAcceptPostWithCorrectCredentials = function() { 124 | var request = new MockRequest(digestApp); 125 | var response = doRequestWithDigestAuth(request, 'POST', '/', 'Alice', 'correct-password'); 126 | 127 | assert.eq(200, response.status); 128 | assert.eq("Hi Alice", response.body.toString()); 129 | }; 130 | 131 | // should return application output for PUT when correct credentials given 132 | exports.testAcceptPutWithCorrectCredentials = function() { 133 | var request = new MockRequest(digestApp); 134 | var response = doRequestWithDigestAuth(request, 'PUT', '/', 'Alice', 'correct-password'); 135 | 136 | assert.eq(200, response.status); 137 | assert.eq("Hi Alice", response.body.toString()); 138 | }; 139 | 140 | // should challenge when no credentials are specified 141 | exports.testChallengeWhenNoCredentials = function() { 142 | var request = new MockRequest(digestApp); 143 | var response = doRequest(request, 'GET', '/'); 144 | assertDigestAuthChallenge(response); 145 | }; 146 | 147 | // should challenge if incorrect username given 148 | exports.testChallengeWhenIncorrectUsername = function() { 149 | var request = new MockRequest(digestApp); 150 | var response = doRequestWithDigestAuth(request, 'GET', '/', 'Fred', 'correct-password'); 151 | assertDigestAuthChallenge(response); 152 | }; 153 | 154 | // should challenge if incorrect password given 155 | exports.testChallengeWhenIncorrectPassword = function() { 156 | var request = new MockRequest(digestApp); 157 | var response = doRequestWithDigestAuth(request, 'GET', '/', 'Alice', 'incorrect-password'); 158 | assertDigestAuthChallenge(response); 159 | }; 160 | 161 | // should return 400 Bad Request if incorrect scheme given 162 | exports.testReturnBadRequestWhenIncorrectScheme = function() { 163 | var request = new MockRequest(digestApp); 164 | var response = doRequest(request, 'GET', '/', {'HTTP_AUTHORIZATION': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}); 165 | assertBadRequest(response); 166 | }; 167 | 168 | // should return 400 Bad Request if incorrect uri given 169 | exports.testReturnBadRequestWhenIncorrectUri = function() { 170 | var request = new MockRequest(digestApp); 171 | var response = doRequestWithDigestAuth(request, 'GET', '/', 'Alice', 'correct-password', {uri: '/foo'}); 172 | assertBadRequest(response); 173 | }; 174 | 175 | // should return 400 Bad Request if incorrect qop given 176 | exports.testReturnBadRequestWhenIncorrectQop = function() { 177 | var request = new MockRequest(digestApp); 178 | var response = doRequestWithDigestAuth(request, 'GET', '/', 'Alice', 'correct-password', {qop: 'auth-int'}); 179 | assertBadRequest(response); 180 | }; 181 | 182 | // should challenge if stale nonce given 183 | exports.testChallengeWhenStaleNonce = function() { 184 | DigestNonce.Nonce.prototype.timeLimit = 1; // 1 millisecond 185 | 186 | var request = new MockRequest(digestApp); 187 | var response = doRequestWithDigestAuth(request, 'GET', '/', 'Alice', 'correct-password', {wait: 1}); 188 | 189 | assertDigestAuthChallenge(response); 190 | assert.isTrue(response.headers['WWW-Authenticate'].search(/stale=true/) != -1); 191 | 192 | //delete timeLimit for future tests 193 | delete DigestNonce.Nonce.prototype.timeLimit; 194 | }; 195 | 196 | // should not require credentials for unprotected path 197 | exports.testAcceptForUnprotectedPath = function() { 198 | var request = new MockRequest(partiallyProtectedApp); 199 | var response = doRequest(request, 'GET', '/'); 200 | 201 | assert.eq(200, response.status); 202 | }; 203 | 204 | // should challenge for protected path 205 | exports.testChallengeForProtectedPath = function() { 206 | var request = new MockRequest(partiallyProtectedApp); 207 | var response = doRequest(request, 'GET', '/protected'); 208 | 209 | assertDigestAuthChallenge(response); 210 | }; 211 | 212 | // should accept correct credentials for protected path 213 | exports.testAcceptCorrectCredentialsForProtectedPath = function() { 214 | var request = new MockRequest(partiallyProtectedApp); 215 | var response = doRequestWithDigestAuth(request, 'GET', '/protected', 'Alice', 'correct-password'); 216 | 217 | assert.eq(200, response.status); 218 | assert.eq("Hi Alice", response.body.toString()); 219 | }; 220 | 221 | // should challenge incorrect credentials for protected path 222 | exports.testChallengeIncorrectCredentialsForProtectedPath = function() { 223 | var request = new MockRequest(partiallyProtectedApp); 224 | var response = doRequestWithDigestAuth(request, 'GET', '/protected', 'Alice', 'incorrect-password'); 225 | 226 | assertDigestAuthChallenge(response); 227 | }; -------------------------------------------------------------------------------- /tests/jack/utils-tests.js: -------------------------------------------------------------------------------- 1 | var assert = require("test/assert"), 2 | Utils = require("jack/utils"), 3 | MockRequest = require("jack/mock").MockRequest, 4 | Request = require("jack").Request, 5 | File = require("file"), 6 | ByteIO = require("io").ByteIO; 7 | 8 | exports.testUnescape = function() { 9 | assert.isEqual("fobar", Utils.unescape("fo%3Co%3Ebar")); 10 | assert.isEqual("a space", Utils.unescape("a%20space")); 11 | assert.isEqual("a+space", Utils.unescape("a+space")); 12 | assert.isEqual("a+space", Utils.unescape("a+space", false)); 13 | assert.isEqual("a space", Utils.unescape("a+space", true)); 14 | assert.isEqual("q1!2\"'w$5&7/z8)?\\", Utils.unescape("q1%212%22%27w%245%267%2Fz8%29%3F%5C")); 15 | } 16 | 17 | // [ wonkyQS, canonicalQS, obj ] 18 | var qsTestCases = [ 19 | ["foo=bar", "foo=bar", {"foo" : "bar"}], 20 | ["foo=bar&foo=quux", "foo%5B%5D=bar&foo%5B%5D=quux", {"foo" : ["bar", "quux"]}], 21 | ["foo=1&bar=2", "foo=1&bar=2", {"foo" : "1", "bar" : "2"}], 22 | ["my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F", "my%20weird%20field=q1!2%22'w%245%267%2Fz8)%3F", {"my weird field" : "q1!2\"'w$5&7/z8)?" }], 23 | ["foo%3Dbaz=bar", "foo%3Dbaz=bar", {"foo=baz" : "bar"}], 24 | ["foo=baz=bar", "foo=baz%3Dbar", {"foo" : "baz=bar"}], 25 | ["str=foo&arr[]=1&arr[]=2&arr[]=3&obj[a]=bar&obj[b][]=4&obj[b][]=5&obj[b][]=6&obj[b][]=&obj[c][]=4&obj[c][]=5&obj[c][][somestr]=baz&obj[objobj][objobjstr]=blerg&somenull=&undef=", "str=foo&arr%5B%5D=1&arr%5B%5D=2&arr%5B%5D=3&obj%5Ba%5D=bar&obj%5Bb%5D%5B%5D=4&obj%5Bb%5D%5B%5D=5&obj%5Bb%5D%5B%5D=6&obj%5Bb%5D%5B%5D=&obj%5Bc%5D%5B%5D=4&obj%5Bc%5D%5B%5D=5&obj%5Bc%5D%5B%5D%5Bsomestr%5D=baz&obj%5Bobjobj%5D%5Bobjobjstr%5D=blerg&somenull=&undef=", { 26 | "str":"foo", 27 | "arr":["1","2","3"], 28 | "obj":{ 29 | "a":"bar", 30 | "b":["4","5","6",""], 31 | "c":["4","5",{"somestr":"baz"}], 32 | "objobj":{"objobjstr":"blerg"} 33 | }, 34 | "somenull":"", 35 | "undef":"" 36 | }], 37 | ["foo[bar][bla]=baz&foo[bar][bla]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}], 38 | ["foo[bar][][bla]=baz&foo[bar][][bla]=blo", "foo%5Bbar%5D%5B%5D%5Bbla%5D=baz&foo%5Bbar%5D%5B%5D%5Bbla%5D=blo", {"foo":{"bar":[{"bla":"baz"},{"bla":"blo"}]}}], 39 | ["foo[bar][bla][]=baz&foo[bar][bla][]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}], 40 | [" foo = bar ", "foo=bar", {"foo":"bar"}] 41 | ]; 42 | var qsColonTestCases = [ 43 | ["foo:bar", "foo:bar", {"foo":"bar"}], 44 | ["foo:bar;foo:quux", "foo%5B%5D:bar;foo%5B%5D:quux", {"foo" : ["bar", "quux"]}], 45 | ["foo:1&bar:2;baz:quux", "foo:1%26bar%3A2;baz:quux", {"foo":"1&bar:2", "baz":"quux"}], 46 | ["foo%3Abaz:bar", "foo%3Abaz:bar", {"foo:baz":"bar"}], 47 | ["foo:baz:bar", "foo:baz%3Abar", {"foo":"baz:bar"}] 48 | ]; 49 | exports.testParseQuery = function() { 50 | qsTestCases.forEach(function (testCase) { 51 | assert.isSame(testCase[2], Utils.parseQuery(testCase[0])); 52 | }); 53 | qsColonTestCases.forEach(function (testCase) { 54 | assert.isSame(testCase[2], Utils.parseQuery(testCase[0], ";", ":")) 55 | }); 56 | } 57 | exports.testToQueryString = function () { 58 | qsTestCases.forEach(function (testCase) { 59 | assert.isSame(testCase[1], Utils.toQueryString(testCase[2])); 60 | }); 61 | qsColonTestCases.forEach(function (testCase) { 62 | assert.isSame(testCase[1], Utils.toQueryString(testCase[2], ";", ":")); 63 | }); 64 | }; 65 | 66 | // specify "should return nil if content type is not multipart" do 67 | exports.testNotMultipart = function() { 68 | var env = MockRequest.envFor(null, "/", { "CONTENT_TYPE" : "application/x-www-form-urlencoded" }); 69 | assert.isNull(Utils.parseMultipart(env)); 70 | } 71 | 72 | // specify "should parse multipart upload with text file" do 73 | exports.testMultipart = function() { 74 | var env = MockRequest.envFor(null, "/", multipartFixture("text")); 75 | var params = Utils.parseMultipart(env); 76 | 77 | assert.isEqual("Larry", params["submit-name"]); 78 | assert.isEqual("text/plain", params["files"]["type"]); 79 | assert.isEqual("file1.txt", params["files"]["filename"]); 80 | assert.isEqual( 81 | "Content-Disposition: form-data; " + 82 | "name=\"files\"; filename=\"file1.txt\"\r\n" + 83 | "Content-Type: text/plain\r\n", 84 | params["files"]["head"]); 85 | assert.isEqual("files", params["files"]["name"]); 86 | //assert.isEqual("contents", params["files"]["tempfile"]); 87 | } 88 | 89 | //specify "should parse multipart upload with nested parameters" do 90 | 91 | exports.testMultipartNested = function() { 92 | var env = MockRequest.envFor(null, "/", multipartFixture("nested")) 93 | var params = Utils.parseMultipart(env); 94 | 95 | assert.isEqual("Larry", params["foo"]["submit-name"]); 96 | assert.isEqual("text/plain", params["foo"]["files"]["type"]); 97 | assert.isEqual("file1.txt", params["foo"]["files"]["filename"]); 98 | assert.isEqual( 99 | "Content-Disposition: form-data; " + 100 | "name=\"foo[files]\"; filename=\"file1.txt\"\r\n" + 101 | "Content-Type: text/plain\r\n", 102 | params["foo"]["files"]["head"]); 103 | assert.isEqual("foo[files]", params["foo"]["files"]["name"]); 104 | assert.isEqual("contents\n", File.read(params["foo"]["files"]["tempfile"])); 105 | // TODO updated this test to add "\n" -- this is probably not the best behavior 106 | } 107 | 108 | // specify "should parse multipart upload with binary file" do 109 | exports.testMultipartBinaryFile = function() { 110 | var env = MockRequest.envFor(null, "/", multipartFixture("binary")); 111 | var params = Utils.parseMultipart(env); 112 | 113 | assert.isEqual("Larry", params["submit-name"]); 114 | assert.isEqual("image/png", params["files"]["type"]); 115 | assert.isEqual("rack-logo.png", params["files"]["filename"]); 116 | assert.isEqual( 117 | "Content-Disposition: form-data; " + 118 | "name=\"files\"; filename=\"rack-logo.png\"\r\n" + 119 | "Content-Type: image/png\r\n", 120 | params["files"]["head"]); 121 | assert.isEqual("files", params["files"]["name"]); 122 | assert.isEqual(26473, File.read(params["files"]["tempfile"], "b").length); 123 | } 124 | 125 | // specify "should parse multipart upload with empty file" do 126 | exports.testMultipartEmptyFile = function() { 127 | var env = MockRequest.envFor(null, "/", multipartFixture("empty")); 128 | var params = Utils.parseMultipart(env); 129 | 130 | assert.isEqual("Larry", params["submit-name"]); 131 | assert.isEqual("text/plain", params["files"]["type"]); 132 | assert.isEqual("file1.txt", params["files"]["filename"]); 133 | assert.isEqual( 134 | "Content-Disposition: form-data; " + 135 | "name=\"files\"; filename=\"file1.txt\"\r\n" + 136 | "Content-Type: text/plain\r\n", 137 | params["files"]["head"]); 138 | assert.isEqual("files", params["files"]["name"]); 139 | assert.isEqual("", File.read(params["files"]["tempfile"])); 140 | } 141 | 142 | // specify "should not include file params if no file was selected" do 143 | exports.testMultipartNoFile = function() { 144 | var env = MockRequest.envFor(null, "/", multipartFixture("none")); 145 | var params = Utils.parseMultipart(env); 146 | 147 | assert.isEqual("Larry", params["submit-name"]); 148 | assert.isEqual(params["files"], ""); // this behavior changes with the new parser 149 | //params.keys.should.not.include "files" 150 | } 151 | 152 | // specify "should parse IE multipart upload and clean up filename" do 153 | exports.testMultipartIEFile = function() { 154 | var env = MockRequest.envFor(null, "/", multipartFixture("ie")); 155 | var params = Utils.parseMultipart(env); 156 | 157 | assert.isEqual("text/plain", params["files"]["type"]); 158 | assert.isEqual("file1.txt", params["files"]["filename"]); 159 | assert.isEqual( 160 | "Content-Disposition: form-data; " + 161 | "name=\"files\"; " + 162 | 'filename="C:\\Documents and Settings\\Administrator\\Desktop\\file1.txt"' + 163 | "\r\nContent-Type: text/plain\r\n", 164 | params["files"]["head"]); 165 | assert.isEqual("files", params["files"]["name"]); 166 | assert.isEqual("contents", File.read(params["files"]["tempfile"], "b").decodeToString()); 167 | } 168 | 169 | exports.testBinaryNonMultipart = function() { 170 | var env = MockRequest.envFor("post", "/upload", fixture(fixtureFile("rack-logo.jpg"), "text/html")); 171 | var request = new Request(env); 172 | request.params(); // force evaluation of the body of the request 173 | 174 | // test it twice to ensure it's cached (or rewound?) 175 | assert.isEqual(15124, request.body().length); 176 | assert.isEqual(15124, request.body().length); 177 | }; 178 | 179 | function fixture(file, contentType) { 180 | var data = File.read(file, 'rb'); 181 | var length = data.length; 182 | 183 | return { 184 | "CONTENT_TYPE": contentType, 185 | "CONTENT_LENGTH": length.toString(10), 186 | "jsgi.body": new ByteIO(data) 187 | }; 188 | } 189 | 190 | function multipartFixture(name) { 191 | return fixture(multipartFile(name), "multipart/form-data; boundary=AaB03x"); 192 | } 193 | 194 | function multipartFile(name) { 195 | return File.join(File.dirname(module.path), "multipart", name); 196 | } 197 | 198 | function fixtureFile(name) { 199 | return File.join(File.dirname(module.path), "fixtures", name); 200 | } 201 | 202 | if (require.main === module.id) 203 | require("test/runner").run(exports); 204 | 205 | -------------------------------------------------------------------------------- /lib/jack/lint.js: -------------------------------------------------------------------------------- 1 | var utils = require("./utils"); 2 | 3 | var Lint = exports.Lint = function(app) { 4 | return function(request) { 5 | return (new Lint.Context(app)).run(request); 6 | } 7 | } 8 | 9 | Lint.Context = function(app) { 10 | this.app = app; 11 | } 12 | 13 | Lint.Context.prototype.run = function(request) { 14 | if (!request) 15 | throw new Error("No request environment provided"); 16 | 17 | this.checkRequest(request); 18 | 19 | var response = this.app(request); 20 | 21 | this.body = response.body; 22 | 23 | if (typeof this.body === "string") 24 | throw new Error("Body must implement forEach, String support deprecated."); 25 | 26 | this.checkStatus(response.status); 27 | this.checkHeaders(response.headers); 28 | this.checkContentType(response.status, response.headers); 29 | this.checkContentLength(response.status, response.headers, request); 30 | 31 | return { 32 | status : response.status, 33 | headers : response.headers, 34 | body : this 35 | }; 36 | } 37 | 38 | Lint.Context.prototype.forEach = function(block) { 39 | return this.body.forEach(function(part) { 40 | if (part === null || part === undefined || typeof part.toByteString !== "function") 41 | throw new Error("Body yielded value that can't be converted to ByteString ("+(typeof part)+","+(typeof part.toByteString)+"): " + part); 42 | block(part); 43 | }); 44 | } 45 | 46 | Lint.Context.prototype.close = function() { 47 | if (this.body.close) 48 | return this.body.close(); 49 | } 50 | 51 | Lint.Context.prototype.checkRequest = function(request) { 52 | if (request && typeof request !== "object" || request.constructor !== Object) 53 | throw new Error("request is not a hash"); 54 | 55 | 56 | ["method","host","port","queryString","body","scheme","jsgi" 57 | ].forEach(function(key) { 58 | if (request[key] === undefined) 59 | throw new Error("request missing required key " + key); 60 | }); 61 | 62 | ["version","errors","multithread","multiprocess","runOnce","ext" 63 | ].forEach(function(key) { 64 | if (request.jsgi[key] === undefined) 65 | throw new Error("request.jsgi missing required key " + key); 66 | }) 67 | 68 | // The request environment must not contain HTTP_* keys 69 | for (var key in request) { 70 | if (key.indexOf("HTTP_") === 0) 71 | throw new Error("request contains 0.2 header key: " + key); 72 | }; 73 | 74 | /* FIXME 75 | // The CGI keys (named without a period) must have String values. 76 | for (var key in request) 77 | if (key.indexOf(".") == -1) 78 | if (typeof request[key] !== "string") 79 | throw new Error("request variable " + key + " has non-string value " + request[key]); 80 | */ 81 | //TODO request.version must be an array of Integers 82 | // * jsgi.version must be an array of Integers. 83 | if (typeof request.jsgi.version !== "object" && !Array.isArray(request.jsgi.version)) 84 | throw new Error("request.jsgi.version must be an Array, was " + request.jsgi.version); 85 | 86 | // * request.scheme must either be +http+ or +https+. 87 | if (request.scheme !== "http" && request.scheme !== "https") 88 | throw new Error("request.scheme unknown: " + request.scheme); 89 | 90 | // * There must be a valid input stream in request.body. 91 | this.checkInput(request.body); 92 | // * There must be a valid error stream in request.jsgi.errors. 93 | this.checkError(request.jsgi.errors); 94 | 95 | // * The REQUEST_METHOD must be a valid token. 96 | if (!(/^[0-9A-Za-z!\#$%&'*+.^_`|~-]+$/).test(request.method)) 97 | throw new Error("request.method unknown: " + request.method); 98 | 99 | // * The SCRIPT_NAME, if non-empty, must start with / 100 | if (request.scriptName && request.scriptName.charAt(0) !== "/") 101 | throw new Error("request.scriptName must start with /"); 102 | 103 | // * The PATH_INFO, if non-empty, must start with / 104 | if (request.pathInfo && request.pathInfo.charAt(0) !== "/") 105 | throw new Error("request.pathInfo must start with /"); 106 | 107 | // * The CONTENT_LENGTH, if given, must consist of digits only. 108 | if (request.headers["content-length"] !== undefined && !(/^\d+$/).test(request.headers["content-length"])) 109 | throw new Error("Invalid content-length: " + request.headers["content-length"]); 110 | 111 | // * One of scriptName or pathInfo must be 112 | // set. pathInfo should be / if 113 | // scriptName is empty. 114 | if (request.scriptName === undefined && request.pathInfo === undefined) 115 | throw new Error("One of scriptName or pathInfo must be set (make pathInfo '/' if scriptName is empty)") 116 | 117 | // scriptName never should be /, but instead be empty. 118 | if (request.scriptName === "/") 119 | throw new Error("scriptName cannot be '/', make it '' and pathInfo '/'") 120 | } 121 | Lint.Context.prototype.checkInput = function(input) { 122 | // FIXME: 123 | /*["gets", "forEach", "read"].forEach(function(method) { 124 | if (typeof input[method] !== "function") 125 | throw new Error("jsgi.input " + input + " does not respond to " + method); 126 | });*/ 127 | } 128 | Lint.Context.prototype.checkError = function(error) { 129 | ["print", "write", "flush"].forEach(function(method) { 130 | if (typeof error[method] !== "function") 131 | throw new Error("jsgi.error " + error + " does not respond to " + method); 132 | }); 133 | } 134 | Lint.Context.prototype.checkStatus = function(status) { 135 | if (!status >= 100) 136 | throw new Error("Status must be integer >= 100"); 137 | } 138 | Lint.Context.prototype.checkHeaders = function(headers) { 139 | for (var key in headers) { 140 | var value = headers[key]; 141 | // The header keys must be Strings. 142 | if (typeof key !== "string") 143 | throw new Error("header key must be a string, was " + key); 144 | 145 | // The header must not contain a +Status+ key, 146 | if (key.toLowerCase() === "status") 147 | throw new Error("header must not contain Status"); 148 | // contain keys with : or newlines in their name, 149 | if ((/[:\n]/).test(key)) 150 | throw new Error("header names must not contain : or \\n"); 151 | // contain keys names that end in - or _, 152 | if ((/[-_]$/).test(key)) 153 | throw new Error("header names must not end in - or _"); 154 | // but only contain keys that consist of 155 | // letters, digits, _ or - and start with a letter. 156 | if (!(/^[a-zA-Z][a-zA-Z0-9_-]*$/).test(key)) 157 | throw new Error("invalid header name: " + key); 158 | // The values of the header must be a string or respond to #forEach. 159 | if (typeof value !== "string" && typeof value.forEach !== "function") 160 | throw new Error("header values must be strings or response to forEach. The value of '" + key + "' is invalid.") //FIXME 161 | 162 | value.split("\n").forEach(function(item) { 163 | // must not contain characters below 037. 164 | if ((/[\000-\037]/).test(item)) 165 | throw new Error("invalid header value " + key + ": " + item); 166 | }); 167 | } 168 | } 169 | Lint.Context.prototype.checkContentType = function(status, headers) { 170 | var contentType = headers["content-type"], 171 | noBody = utils.STATUS_WITH_NO_ENTITY_BODY(parseInt(status)); 172 | 173 | if (noBody && contentType) 174 | throw new Error("A content-type header found in " + status + " response, not allowed"); 175 | if (!noBody && !contentType) 176 | throw new Error("No content-type header found"); 177 | } 178 | Lint.Context.prototype.checkContentLength = function(status, headers, request) { 179 | var chunked_response = headers["transfer-encoding"] && headers["transfer-encoding"] !== "identity"; 180 | 181 | var value = headers["content-length"]; 182 | if (value) { 183 | // There must be a content-length, except when the 184 | // +Status+ is 1xx, 204 or 304, in which case there must be none 185 | // given. 186 | if (utils.STATUS_WITH_NO_ENTITY_BODY(parseInt(status))) 187 | throw new Error("A content-length header found in " + status + " response, not allowed"); 188 | 189 | if (chunked_response) 190 | throw new Error("A content-length header should not be used if body is chunked"); 191 | 192 | if (typeof value !== "string") 193 | throw new Error("A content-length header value must be be singular") 194 | 195 | var bytes = 0, 196 | string_body = true; 197 | 198 | this.body.forEach(function(part) { 199 | if (typeof part !== "string") 200 | string_body = false; 201 | bytes += (part && part.length) ? part.length : 0; 202 | }); 203 | 204 | if (request.method === "HEAD") 205 | { 206 | if (bytes !== 0) 207 | throw new Error("Response body was given for HEAD request, but should be empty"); 208 | } 209 | else if (string_body) 210 | { 211 | if (value !== bytes.toString()) 212 | throw new Error("The content-length header was " + value + ", but should be " + bytes); 213 | } 214 | } 215 | else { 216 | if (!chunked_response && (typeof this.body === "string" || Array.isArray(this.body))) 217 | if (!utils.STATUS_WITH_NO_ENTITY_BODY(parseInt(status))) 218 | throw new Error('No content-length header found'); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /lib/jack/handler/servlet.js: -------------------------------------------------------------------------------- 1 | // Similar in structure to Rack's Mongrel handler. 2 | // All generic Java servlet code should go in here. 3 | // Specific server code should go in separate handlers (i.e. jetty.js, etc) 4 | 5 | // options.useEventQueueWorker set to true will start a thread for each server thread to 6 | // handler the event queue. This allows events to be executed at any time 7 | // for a request thread, (while still maintaining the JS single-thread per 8 | // worker model) 9 | // options.useEventQueueWorker set to false will not start threads 10 | // and events in the queue will only be processed after request is processed. 11 | // options.useEventQueueWorker must be false on systems like GAE 12 | // that do not allow threads 13 | var workerEngine = require("worker-engine"), 14 | worker = require("worker"), 15 | IO = require("io").IO, 16 | file = require("file"), 17 | when = require("promise").when, 18 | HashP = require("hashp").HashP; 19 | 20 | var Servlet = exports.Servlet = function(options) { 21 | this.options = options; 22 | } 23 | 24 | Servlet.prototype.process = function(request, response) { 25 | Servlet.process(this.options, request, response); 26 | } 27 | var processors = java.lang.ThreadLocal(); 28 | 29 | Servlet.process = function(options, request, response) { 30 | var processor = processors.get(); 31 | if(!processor){ 32 | var workerGlobal = worker.createEnvironment(); 33 | 34 | options.server = "servlet"; 35 | var processor = workerGlobal.require("jackup").start(options); 36 | processors.set(processor); 37 | 38 | } 39 | processor(request, response); 40 | 41 | }; 42 | 43 | var queue = require("event-loop"); 44 | // get the continuation method if available 45 | var getContinuation = org.mortbay.util.ajax.ContinuationSupport.getContinuation; 46 | 47 | exports.run = function(app, options){ 48 | if(options.useEventQueueWorker){ 49 | workerEngine.spawn(function(){ 50 | while(true){ 51 | var func = queue.getNextEvent(); 52 | try{ 53 | sync(func)(); 54 | }catch(e){ 55 | queue.defaultErrorReporter(e); 56 | } 57 | } 58 | 59 | }); 60 | } 61 | 62 | return sync(function(request, response){ 63 | if(typeof getContinuation == "function"){ 64 | var continuation = getContinuation(request, null); 65 | if(continuation.isResumed()){ 66 | return; 67 | } 68 | } 69 | 70 | var req = {headers:{}}; 71 | 72 | // copy HTTP headers over, converting where appropriate 73 | for (var e = request.getHeaderNames(); e.hasMoreElements();) 74 | { 75 | var name = String(e.nextElement()), 76 | value = String(request.getHeader(name)), // FIXME: only gets the first of multiple 77 | key = name.toLowerCase(); 78 | 79 | req.headers[key] = value; 80 | } 81 | 82 | req.scriptName = String(request.getContextPath() || ""); 83 | req.pathInfo = request.getPathInfo(); 84 | if(!req.pathInfo){ 85 | // in servlet filters the pathInfo will always be null, so we have to compute it from the request URI 86 | var path = request.getRequestURI(); 87 | req.pathInfo = String(path.substring(String(request.getContextPath()).length) || ""); 88 | } 89 | else{ 90 | req.pathInfo = String(req.pathInfo); 91 | } 92 | req.method = String(request.getMethod() || ""); 93 | req.host = String(request.getServerName() || ""); 94 | req.port = String(request.getServerPort() || ""); 95 | req.queryString = String(request.getQueryString() || ""); 96 | req.version = String(request.getProtocol() || ""); 97 | req.scheme = request.isSecure() ? "https" : "http"; 98 | req.env = {}; // don't know what actually goes in here 99 | 100 | req.remoteHost = String(request.getRemoteHost() || ""); 101 | req.body = new IO(request.getInputStream(), null), 102 | req.jsgi = { 103 | "version": [0,3], 104 | "errors": system.stderr, 105 | "async": true, 106 | "multithread": false, 107 | "multiprocess": true, 108 | "runOnce": false}; 109 | 110 | req["x-sendfile"] = "yes"; 111 | var res = app(req); 112 | var finished; 113 | var output, responseStarted = false; 114 | // use the promise manager to determine when the app is done 115 | // in a normal sync request, it will just execute the fulfill 116 | // immediately 117 | when(res, function(res){ 118 | // success handler 119 | finished = true; 120 | 121 | try{ 122 | handleResponse(res); 123 | } 124 | catch(e){ 125 | print(String((e.rhinoException && e.rhinoException.printStackTrace()) || (e.name + ": " + e.message))); 126 | response.getOutputStream().write(e); 127 | response.getOutputStream().close(); 128 | } 129 | // finished 130 | if(continuation){ 131 | continuation.resume(); 132 | } 133 | }, function(error){ 134 | finished = true; 135 | // unhandled error 136 | handleResponse({status:500, headers:{}, body:[error.message]}); 137 | }, 138 | // progress handler 139 | handleResponse); 140 | 141 | function handleResponse(res){ 142 | if(!responseStarted){ 143 | responseStarted = true; 144 | // set the status 145 | response.setStatus(res.status); 146 | 147 | // check to see if X-Sendfile was used, remove the header 148 | var sendfilePath = null; 149 | if (HashP.includes(res.headers, "X-Sendfile")) { 150 | sendfilePath = HashP.unset(res.headers, "X-Sendfile"); 151 | HashP.set(res.headers, "Content-Length", String(file.size(sendfilePath))); 152 | } 153 | 154 | // set the headers 155 | for (var key in res.headers) { 156 | ('' + res.headers[key]).split("\n").forEach(function(value) { 157 | response.addHeader(key, value); 158 | }); 159 | } 160 | 161 | // determine if the response should be chunked (FIXME: need a better way?) 162 | var chunked = HashP.includes(res.headers, "Transfer-Encoding") && HashP.get(res.headers, "Transfer-Encoding") !== 'identity'; 163 | 164 | output = new IO(null, response.getOutputStream()); 165 | 166 | // X-Sendfile send 167 | if (sendfilePath) { 168 | var cIn = new java.io.FileInputStream(sendfilePath).getChannel(), 169 | cOut = response.getByteChannel(); 170 | 171 | cIn.transferTo(0, cIn.size(), cOut); 172 | 173 | cIn.close(); 174 | cOut.close(); 175 | } 176 | else { 177 | try{ 178 | // output the body, flushing after each write if it's chunked 179 | var possiblePromise = res.body.forEach(function(chunk) { 180 | if (!sendfilePath) { 181 | //output.write(new java.lang.String(chunk).getBytes("US-ASCII")); 182 | //output.write(chunk, "US-ASCII"); 183 | // with async/promises, it would actually be more ideal to set the headers 184 | // and status here before the first write, so that a promise could suspend 185 | // and then set status when it resumed 186 | 187 | output.write(chunk); 188 | 189 | if (chunked) 190 | output.flush(); 191 | } 192 | }); 193 | 194 | // check to see if the return value is a promise 195 | // alternately we could check with if (possiblePromise instanceof require("promise").Promise) 196 | if (possiblePromise && typeof possiblePromise.then == "function") { 197 | // its a promise, don't close the output until it is fulfilled 198 | possiblePromise.then(function() { 199 | // fulfilled, we are done now 200 | output.close(); 201 | }, 202 | function(e) { 203 | // an error, just write the error and finish 204 | output.write(e.message); 205 | output.close(); 206 | }); 207 | } 208 | else { 209 | // not a promise, regular sync request, we are done now 210 | output.close(); 211 | } 212 | } 213 | catch(e){ 214 | output.write(String((e.rhinoException && e.rhinoException.printStackTrace()) || (e.name + ": " + e.message))); 215 | if(chunked){ 216 | output.flush(); 217 | } 218 | } 219 | } 220 | } 221 | if(finished){ 222 | // send it off 223 | output.close(); 224 | } 225 | // process all the events in the queue 226 | if(!options.useEventQueueWorker){ 227 | while(queue.hasPendingEvents()){ 228 | queue.processNextEvent(); 229 | } 230 | } 231 | if(!finished && continuation){ 232 | continuation.suspend(request.getSession().getMaxInactiveInterval() * 500); 233 | } 234 | 235 | } 236 | }); 237 | } -------------------------------------------------------------------------------- /lib/jack/utils.js: -------------------------------------------------------------------------------- 1 | var file = require("file"), 2 | ByteString = require("binary").ByteString, 3 | ByteIO = require("io").ByteIO; 4 | 5 | // Every standard HTTP code mapped to the appropriate message. 6 | // Stolen from Rack which stole from Mongrel 7 | exports.HTTP_STATUS_CODES = { 8 | 100 : 'Continue', 9 | 101 : 'Switching Protocols', 10 | 102 : 'Processing', 11 | 200 : 'OK', 12 | 201 : 'Created', 13 | 202 : 'Accepted', 14 | 203 : 'Non-Authoritative Information', 15 | 204 : 'No Content', 16 | 205 : 'Reset Content', 17 | 206 : 'Partial Content', 18 | 207 : 'Multi-Status', 19 | 300 : 'Multiple Choices', 20 | 301 : 'Moved Permanently', 21 | 302 : 'Found', 22 | 303 : 'See Other', 23 | 304 : 'Not Modified', 24 | 305 : 'Use Proxy', 25 | 307 : 'Temporary Redirect', 26 | 400 : 'Bad Request', 27 | 401 : 'Unauthorized', 28 | 402 : 'Payment Required', 29 | 403 : 'Forbidden', 30 | 404 : 'Not Found', 31 | 405 : 'Method Not Allowed', 32 | 406 : 'Not Acceptable', 33 | 407 : 'Proxy Authentication Required', 34 | 408 : 'Request Timeout', 35 | 409 : 'Conflict', 36 | 410 : 'Gone', 37 | 411 : 'Length Required', 38 | 412 : 'Precondition Failed', 39 | 413 : 'Request Entity Too Large', 40 | 414 : 'Request-URI Too Large', 41 | 415 : 'Unsupported Media Type', 42 | 416 : 'Request Range Not Satisfiable', 43 | 417 : 'Expectation Failed', 44 | 422 : 'Unprocessable Entity', 45 | 423 : 'Locked', 46 | 424 : 'Failed Dependency', 47 | 500 : 'Internal Server Error', 48 | 501 : 'Not Implemented', 49 | 502 : 'Bad Gateway', 50 | 503 : 'Service Unavailable', 51 | 504 : 'Gateway Timeout', 52 | 505 : 'HTTP Version Not Supported', 53 | 507 : 'Insufficient Storage' 54 | }; 55 | 56 | exports.HTTP_STATUS_MESSAGES = {}; 57 | for (var code in exports.HTTP_STATUS_CODES) 58 | exports.HTTP_STATUS_MESSAGES[exports.HTTP_STATUS_CODES[code]] = parseInt(code); 59 | 60 | exports.STATUS_WITH_NO_ENTITY_BODY = function(status) { return (status >= 100 && status <= 199) || status == 204 || status == 304; }; 61 | 62 | exports.responseForStatus = function(status, optMessage) { 63 | if (exports.HTTP_STATUS_CODES[status] === undefined) 64 | throw "Unknown status code"; 65 | 66 | var message = exports.HTTP_STATUS_CODES[status]; 67 | 68 | if (optMessage) 69 | message += ": " + optMessage; 70 | 71 | var body = (message+"\n").toByteString("UTF-8"); 72 | 73 | return { 74 | status : status, 75 | headers : { "Content-Type" : "text/plain", "Content-Length" : String(body.length) }, 76 | body : [body] 77 | }; 78 | } 79 | 80 | exports.parseQuery = require("./querystring").parseQuery; 81 | exports.toQueryString = require("./querystring").toQueryString; 82 | exports.unescape = require("./querystring").unescape; 83 | exports.escape = require("./querystring").escape; 84 | 85 | 86 | var EOL = "\r\n"; 87 | 88 | exports.parseMultipart = function(request, options) { 89 | options = options || {}; 90 | 91 | var match, i, data; 92 | if (request.headers['content-type'] && (match = request.headers['content-type'].match(/^multipart\/form-data.*boundary=\"?([^\";,]+)\"?/m))) { 93 | var boundary = "--" + match[1], 94 | 95 | params = {}, 96 | buf = new ByteString(), 97 | contentLength = parseInt(request.headers['content-length']), 98 | input = request.body, 99 | 100 | boundaryLength = boundary.length + EOL.length, 101 | bufsize = 1024; 102 | 103 | contentLength -= boundaryLength; 104 | 105 | var status = input.read(boundaryLength).decodeToString("UTF-8"); 106 | if (status !== boundary + EOL) 107 | throw new Error("EOFError bad content body"); 108 | 109 | var rx = new RegExp("(?:"+EOL+"+)?"+RegExp.escape(boundary)+"("+EOL+"|--)"); 110 | 111 | while (true) { 112 | var head = null, 113 | body = new ByteIO(), 114 | filename = null, 115 | contentType = null, 116 | name = null, 117 | tempfile = null; 118 | 119 | while (!head || !rx.test(buf.decodeToString())) { 120 | if (!head && (i = buf.decodeToString().indexOf("\r\n\r\n")) > 0) { 121 | head = buf.slice(0, i+2).decodeToString(); 122 | buf = buf.slice(i+4); 123 | 124 | match = head.match(/Content-Disposition:.* filename="?([^\";]*)"?/i); 125 | filename = match && match[1]; 126 | match = head.match(/Content-Type: (.*)\r\n/i); 127 | contentType = match && match[1]; 128 | match = head.match(/Content-Disposition:.* name="?([^\";]*)"?/i); 129 | name = match && match[1]; 130 | 131 | if (filename) { 132 | // TODO: bypass String conversion for files, use Binary/ByteString directly 133 | if (options.nodisk) { 134 | tempfile = null; 135 | body = new ByteIO(); 136 | } else { 137 | if(options.uploadedFilepath){ 138 | tempfile = options.uploadedFilepath(filename, contentType, name); 139 | } 140 | else{ 141 | file.mkdirs("tmp"); 142 | tempfile = "tmp/jackupload-"+Math.round(Math.random()*100000000000000000).toString(16); 143 | } 144 | body = file.open(tempfile, "wb"); 145 | } 146 | } 147 | 148 | continue; 149 | } 150 | 151 | // Save the read body part. 152 | if (head && (boundaryLength + 4 < buf.length)) { 153 | var lengthToWrite = buf.length - (boundaryLength + 4); 154 | body.write(buf.slice(0, lengthToWrite)); 155 | buf = buf.slice(lengthToWrite); 156 | } 157 | 158 | var bytes = input.read(bufsize < contentLength ? bufsize : contentLength); 159 | if (!bytes) 160 | throw new Error("EOFError bad content body"); 161 | if(bytes.length == 0){ 162 | break; 163 | } 164 | //var c = bytes; 165 | var temp = new ByteIO(); 166 | temp.write(buf); 167 | temp.write(bytes); 168 | 169 | buf = temp.toByteString(); 170 | contentLength -= bytes.length; 171 | } 172 | 173 | var startMatch = null; 174 | var isMatched = false; 175 | for (var j=0;j