├── .gitignore
├── README.md
├── example
├── README.md
├── config.js
├── root
│ ├── favicon.ico
│ └── nitro.png
└── src
│ ├── root
│ ├── hello.js
│ ├── index.js
│ ├── redirect.js
│ ├── stream.js
│ └── upload.js
│ ├── templates
│ └── index.html
│ └── wrap.js
├── lib
├── jack
│ ├── contentlength.js
│ ├── contenttype.js
│ ├── hash.js
│ ├── hashp.js
│ ├── head.js
│ ├── methodoverride.js
│ ├── mime.js
│ ├── querystring.js
│ └── utils.js
├── nitro.js
└── nitro
│ ├── cache.js
│ ├── middleware
│ ├── api.js
│ ├── dispatch.js
│ ├── errors.js
│ ├── memcache.js
│ ├── path.js
│ ├── render.js
│ └── setup.js
│ ├── request.js
│ ├── response.js
│ ├── session.js
│ ├── template.js
│ └── utils
│ ├── atom.js
│ └── fileupload.js
├── package.json
└── test
├── nitro
└── middleware
│ ├── dispatch.js
│ └── path.js
└── run.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.local.*
2 | .DS_Store
3 | *.swp
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Nitro
2 | =====
3 |
4 | Nitro provides a library of carefully designed middleware and utilities for creating scalable, standards-compliant Web Applications with JavaScript. Nitro is build on top of Jack/JSGI, CommonJS and Rhino.
5 |
6 | Nitro applications leverage (strict) Web Standards like XHTML/HTML, CSS, HTTP, XML, XSLT, ECMAScript 3.0, MicroFormats, etc. Typically, Nitro applications are a collection of programs that run on the server *and* the client. A control program dispatches work to the application programs and aggregates their output. The application's output is consumed by modern web browsers, web services or other applications through a standard REST interface.
7 |
8 | Nitro is engineered to work great with Google App Engine.
9 |
10 | * Homepage: [http://nitrojs.org/](http://nitrojs.org/)
11 | * Status updates: [http://twitter.com/nitrojs](http://twitter.com/nitrojs)
12 | * Source & Download: [http://github.com/gmosx/nitro/](http://github.com/gmosx/nitro/)
13 | * Documentation: [http://nitrojs.org/docs](http://nitrojs.org/docs)
14 | * Mailing list: [http://groups.google.com/group/nitro-devel](http://groups.google.com/group/nitro-devel)
15 | * Issue tracking: [http://github.com/gmosx/nitro/issues](http://github.com/gmosx/nitro/issues)
16 | * IRC: #nitrojs on [irc.freenode.net](http://freenode.net/)
17 |
18 |
19 | Getting Started
20 | ---------------
21 |
22 | An older version of Nitro was powered by [Narwhal](http://www.narwhaljs.org] but we have switched to [RingoJS](http://www.ringojs.org) due to better support for [Rhino](http://www.mozilla.org/rhino). Still, we 'll try hard to keep future Nitro versions compatible with Narwhal.
23 |
24 | To install Ringo, follow the instructions here:
25 |
26 | [http://ringojs.org/wiki/Getting_Started](http://ringojs.org/wiki/Getting_Started)
27 |
28 | Then, you should install the nitro package:
29 |
30 | $ ringo-admin gmosx/nitro
31 | $ ringo-admin gmosx/normal-template (optional, used in the example)
32 |
33 | Finally, you are ready to run the simple example:
34 |
35 | $ cd example
36 | $ ringo config.js
37 |
38 | The application will start listening at localhost:8080, so use your favourite browser to verify that everything works correctly.
39 |
40 | For a more sophisticated example that implements a simple Blog on Google App Engine have a look at:
41 |
42 | [appengine-blog-example](http://www.nitrojs.org/appenginejs/appengine-blog-example.tar.gz)
43 |
44 | For more details on Nitro, make sure you check out the [documentation](http://www.nitrojs.org/docs).
45 |
46 |
47 | Google App Engine
48 | -----------------
49 |
50 | Nitro applications run great on Google App Engine. Have a look at the [appengine-blog-example](http://www.nitrojs.org/appenginejs/appengine-blog-example.tar.gz) example for a demonstration of using Nitro and [appengine](http://github.com/gmosx/appengine/tree/master) package to develop a simple Blog.
51 |
52 |
53 | Directory structure
54 | -------------------
55 |
56 | /docs:
57 | Contains documentation files in markdown format. The docs are published at www.nitrojs.org/docs
58 |
59 | /lib:
60 | Contains the implementation of the web application framework
61 |
62 | /example:
63 | Contains a simple example
64 |
65 | /test:
66 | Contains unit and functional tests.
67 |
68 |
69 | Related projects
70 | ----------------
71 |
72 | Nitro is an ecosystem of Web Application development tools:
73 |
74 | * [appengine](http://github.com/gmosx/appengine)
75 | * [normal-template](http://github.com/gmosx/normal-template)
76 | * [htmlparser](http://github.com/gmosx/htmlparser)
77 | * [markdown](http://www.github.com/gmosx/markdown)
78 | * [inflection](http://github.com/gmosx/inflection)
79 |
80 | Other related projects:
81 |
82 | * [appengine-blog-example](http://www.nitrojs.org/appenginejs/appengine-blog-example.tar.gz)
83 |
84 |
85 | Credits
86 | -------
87 |
88 | * George Moschovitis, [george.moschovitis@gmail.com](mailto:george.moschovitis@gmail.com)
89 | * Panagiotis Astithas, [pastith@gmail.com](mailto:george.moschovitis@gmail.com)
90 |
91 | This version of Nitro includes files from Jack (http://github.com/tlrobinson/jack).
92 | The copyright belongs to the Jack contributors.
93 |
94 |
95 | License
96 | -------
97 |
98 | Copyright (c) 2009-2010 George Moschovitis, [http://www.gmosx.com](http://www.gmosx.com)
99 |
100 | Permission is hereby granted, free of charge, to any person obtaining a copy
101 | of this software and associated documentation files (the "Software"), to
102 | deal in the Software without restriction, including without limitation the
103 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
104 | sell copies of the Software, and to permit persons to whom the Software is
105 | furnished to do so, subject to the following conditions:
106 |
107 | The above copyright notice and this permission notice shall be included in
108 | all copies or substantial portions of the Software.
109 |
110 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
111 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
112 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
113 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
114 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
115 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
116 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | A simple Nitro example
2 | ======================
3 |
4 | This is a very simple example. To start the application just give:
5 |
6 | $ ringo config.js
7 |
8 | Then browse localhost:8080:
9 |
10 | localhost:8080/
11 | A simple template with one interpolated param
12 |
13 | localhost:8080/hello
14 | The mandatory Hello World
15 |
16 | localhost:8080/stream
17 | A simple streaming example
18 |
19 | There is not much to see in this example so go on and try the more advanced [appengine-blog-example](http://www.nitrojs.org/appenginejs/appengine-blog-example.tar.gz) example!
20 |
--------------------------------------------------------------------------------
/example/config.js:
--------------------------------------------------------------------------------
1 | var Setup = require("nitro/middleware/setup").Setup,
2 | Path = require("nitro/middleware/path").Path,
3 | Errors = require("nitro/middleware/errors").Errors,
4 | Render = require("nitro/middleware/render").Render,
5 | Dispatch = require("nitro/middleware/dispatch").Dispatch;
6 |
7 | var Wrap = require("./src/wrap").Wrap;
8 |
9 | exports.static = ["./root"];
10 |
11 | exports.app = Setup(Path(Errors(Render(Wrap(Dispatch({dispatchRoot: "src/root"})), {templateRoot: "src/templates"})), {scriptRoot: "src/root"}));
12 |
13 | if (require.main == module) {
14 | require("ringo/webapp").main(module.directory);
15 | }
16 |
--------------------------------------------------------------------------------
/example/root/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gmosx/nitro-js/520b75b3d9d94e820a572a705a182a11c7288e52/example/root/favicon.ico
--------------------------------------------------------------------------------
/example/root/nitro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gmosx/nitro-js/520b75b3d9d94e820a572a705a182a11c7288e52/example/root/nitro.png
--------------------------------------------------------------------------------
/example/src/root/hello.js:
--------------------------------------------------------------------------------
1 | exports.GET = function(env) {
2 | return {body: ["Hello World"]};
3 | }
4 |
--------------------------------------------------------------------------------
/example/src/root/index.js:
--------------------------------------------------------------------------------
1 | var Session = require("nitro/session").Session;
2 |
3 | exports.GET = function (env) {
4 | var session = new Session(env);
5 |
6 | var counter = session.counter || 0;
7 | counter += 1;
8 | session.counter = counter;
9 |
10 | return {data: {
11 | time: new Date(),
12 | counter: counter
13 | }};
14 | }
15 |
--------------------------------------------------------------------------------
/example/src/root/redirect.js:
--------------------------------------------------------------------------------
1 | var Response = require("nitro/response").Response;
2 |
3 | exports.GET = function (request) {
4 | return Response.redirect(request.headers.referer);
5 | }
6 |
--------------------------------------------------------------------------------
/example/src/root/stream.js:
--------------------------------------------------------------------------------
1 | var chunked = require("nitro/response").Response.chunked;
2 |
3 | var JThread = Packages.java.lang.Thread;
4 |
5 | var stream = function(write) {
6 | for (var i = 0; i < 50; i++) {
7 | JThread.currentThread().sleep(200);
8 | write("hello world, this is a streaming example for nitro. enjoy the ride! We will be back in 200ms! ");
9 | }
10 | }
11 |
12 | exports.GET = function(env) {
13 | return chunked(stream);
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/example/src/root/upload.js:
--------------------------------------------------------------------------------
1 | var Request = require("nitro/request").Request;
2 |
3 | exports.GET = function(env) {
4 | return {data: {}};
5 | }
6 |
7 | exports.POST = function(env) {
8 | var params = new Request(env).params();
9 |
10 | return {
11 | status: 200,
12 | headers: {
13 | "Content-Type": params.file.type
14 | },
15 | body: [system.fs.read(params.file.tempfile, "b")]
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/example/src/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
A simple example
3 |
4 | The time is: {=time}
5 |
6 |
7 |
15 |
16 |
17 | A simple counter: {=counter}
18 | Reload to increment the counter.
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/src/wrap.js:
--------------------------------------------------------------------------------
1 | // An example wrap middleware.
2 |
3 | exports.Wrap = function(app) {
4 | return function(env) {
5 | print("Responding to " + env["PATH_INFO"]);
6 | return app(env);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/lib/jack/contentlength.js:
--------------------------------------------------------------------------------
1 | var utils = require("./utils"),
2 | HashP = require("./hashp").HashP;
3 |
4 | // Sets the Content-Length header on responses with fixed-length bodies.
5 | var ContentLength = exports.ContentLength = exports.middleware = function (app) {
6 | return function (env) {
7 | var response = app(env);
8 | if (!utils.STATUS_WITH_NO_ENTITY_BODY(response.status) &&
9 | !HashP.includes(response.headers, "Content-Length") &&
10 | !(HashP.includes(response.headers, "Transfer-Encoding") && HashP.get(response.headers, "Transfer-Encoding") !== "identity") &&
11 | typeof response.body.forEach === "function")
12 | {
13 | var newBody = [],
14 | length = 0;
15 |
16 | response.body.forEach(function (part) {
17 | var binary = part.toByteString();
18 | length += binary.length;
19 | newBody.push(binary);
20 | });
21 |
22 | response.body = newBody;
23 |
24 | HashP.set(response.headers, "Content-Length", String(length));
25 | }
26 |
27 | return response;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/jack/contenttype.js:
--------------------------------------------------------------------------------
1 | var HashP = require("./hashp").HashP,
2 | STATUS_WITH_NO_ENTITY_BODY = require("./utils").STATUS_WITH_NO_ENTITY_BODY,
3 | MIME_TYPES = require("./mime").MIME_TYPES,
4 | DEFAULT_TYPE = "text/plain";
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 (env) {
15 | var response = app(env);
16 |
17 | if (!STATUS_WITH_NO_ENTITY_BODY(response.status) && !HashP.get(response.headers, "Content-Type")) {
18 | var contentType = options.contentType;
19 | if (!contentType) {
20 | var extension = env.pathTranslated.match(/(\.[^.]+|)$/)[0];
21 | contentType = options.MIME_TYPES[extension] || MIME_TYPES[extension] || DEFAULT_TYPE;
22 | }
23 | HashP.set(response.headers, "Content-Type", contentType);
24 | }
25 |
26 | return response;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/jack/hash.js:
--------------------------------------------------------------------------------
1 |
2 | // Hash object
3 |
4 | // -- tlrobinson Tom Robinson
5 |
6 | var Hash = exports.Hash = {};
7 |
8 | Hash.merge = function(hash, other) {
9 | var merged = {};
10 | if (hash) Hash.update(merged, hash);
11 | if (other) Hash.update(merged, other);
12 | return merged;
13 | }
14 |
15 | Hash.update = function(hash, other) {
16 | for (var key in other)
17 | hash[key] = other[key];
18 | return hash;
19 | }
20 |
21 | Hash.forEach = function(hash, block) {
22 | for (var key in hash)
23 | block(key, hash[key]);
24 | }
25 |
26 | Hash.map = function(hash, block) {
27 | var result = [];
28 | for (var key in hash)
29 | result.push(block(key, hash[key]));
30 | return result;
31 | }
32 |
--------------------------------------------------------------------------------
/lib/jack/hashp.js:
--------------------------------------------------------------------------------
1 |
2 | // -- tlrobinson Tom Robinson
3 |
4 | var Hash = require("./hash").Hash;
5 |
6 | // HashP : Case Preserving hash, used for headers
7 |
8 | var HashP = exports.HashP = {};
9 |
10 | HashP.get = function(hash, key) {
11 | var ikey = _findKey(hash, key);
12 | if (ikey !== null)
13 | return hash[ikey];
14 | // not found
15 | return undefined;
16 | }
17 |
18 | HashP.set = function(hash, key, value) {
19 | // do case insensitive search, and delete if present
20 | var ikey = _findKey(hash, key);
21 | if (ikey && ikey !== key)
22 | delete hash[ikey];
23 | // set it, preserving key case
24 | hash[key] = value;
25 | }
26 |
27 | HashP.unset = function(hash, key) {
28 | // do case insensitive search, and delete if present
29 | var ikey = _findKey(hash, key),
30 | value;
31 | if (ikey) {
32 | value = hash[ikey];
33 | delete hash[ikey];
34 | }
35 | return value;
36 | }
37 |
38 | HashP.includes = function(hash, key) {
39 | return HashP.get(hash, key) !== undefined
40 | }
41 |
42 | HashP.merge = function(hash, other) {
43 | var merged = {};
44 | if (hash) HashP.update(merged, hash);
45 | if (other) HashP.update(merged, other);
46 | return merged;
47 | }
48 |
49 | HashP.update = function(hash, other) {
50 | for (var key in other)
51 | HashP.set(hash, key, other[key]);
52 | return hash;
53 | }
54 |
55 | HashP.forEach = Hash.forEach;
56 | HashP.map = Hash.map;
57 |
58 | var _findKey = function(hash, key) {
59 | // optimization
60 | if (hash[key] !== undefined)
61 | return key;
62 | // case insensitive search
63 | var key = key.toLowerCase();
64 | for (var i in hash)
65 | if (i.toLowerCase() === key)
66 | return i;
67 | return null;
68 | }
69 |
--------------------------------------------------------------------------------
/lib/jack/head.js:
--------------------------------------------------------------------------------
1 | exports.Head = exports.middleware = function(app) {
2 | return function(env) {
3 | var response = app(env);
4 |
5 | if (env["REQUEST_METHOD"] === "HEAD")
6 | response.body = [];
7 |
8 | return response;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/lib/jack/methodoverride.js:
--------------------------------------------------------------------------------
1 | // -- gmosx George Moschovitis Copyright (C) 2009-2010 MIT License
2 |
3 | var Request = require("nitro/request").Request;
4 |
5 | /**
6 | * Provides Rails-style HTTP method overriding via the _method parameter or X-HTTP-METHOD-OVERRIDE header
7 | * http://code.google.com/apis/gdata/docs/2.0/basics.html#UpdatingEntry
8 | */
9 | exports.MethodOverride = exports.middleware = function (app) {
10 | return function (env) {
11 | // FIXME: what happend when no "content-type"?
12 | if ((env.method == "POST") && (!(env.headers["content-type"] && env.headers["content-type"].match(/^multipart\/form-data/)))) {
13 | var request = new Request(env),
14 | // THINK: only check header to make more lightweight?
15 | method = env.headers[METHOD_OVERRIDE_HEADER] || request.params[METHOD_OVERRIDE_PARAM_KEY];
16 | if (method && METHODS[method.toUpperCase()] === true) {
17 | env["jack.methodoverride.original_method"] = env.requestMethod;
18 | env.method = method.toUpperCase();
19 | }
20 | }
21 | return app(env);
22 | }
23 | }
24 |
25 | var METHODS = {"GET": true, "HEAD": true, "PUT": true, "POST": true, "DELETE": true, "OPTIONS": true};
26 | var METHOD_OVERRIDE_PARAM_KEY = "_method";
27 | //var METHOD_OVERRIDE_HEADER = "X_HTTP_METHOD_OVERRIDE";
28 | var METHOD_OVERRIDE_HEADER = "x-http-method-override";
29 |
--------------------------------------------------------------------------------
/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/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/utils.js:
--------------------------------------------------------------------------------
1 | var FS = require("fs");
2 | // ByteIO = require("io").ByteIO;
3 |
4 | // Every standard HTTP code mapped to the appropriate message.
5 | // Stolen from Rack which stole from Mongrel
6 | exports.HTTP_STATUS_CODES = {
7 | 100 : 'Continue',
8 | 101 : 'Switching Protocols',
9 | 102 : 'Processing',
10 | 200 : 'OK',
11 | 201 : 'Created',
12 | 202 : 'Accepted',
13 | 203 : 'Non-Authoritative Information',
14 | 204 : 'No Content',
15 | 205 : 'Reset Content',
16 | 206 : 'Partial Content',
17 | 207 : 'Multi-Status',
18 | 300 : 'Multiple Choices',
19 | 301 : 'Moved Permanently',
20 | 302 : 'Found',
21 | 303 : 'See Other',
22 | 304 : 'Not Modified',
23 | 305 : 'Use Proxy',
24 | 307 : 'Temporary Redirect',
25 | 400 : 'Bad Request',
26 | 401 : 'Unauthorized',
27 | 402 : 'Payment Required',
28 | 403 : 'Forbidden',
29 | 404 : 'Not Found',
30 | 405 : 'Method Not Allowed',
31 | 406 : 'Not Acceptable',
32 | 407 : 'Proxy Authentication Required',
33 | 408 : 'Request Timeout',
34 | 409 : 'Conflict',
35 | 410 : 'Gone',
36 | 411 : 'Length Required',
37 | 412 : 'Precondition Failed',
38 | 413 : 'Request Entity Too Large',
39 | 414 : 'Request-URI Too Large',
40 | 415 : 'Unsupported Media Type',
41 | 416 : 'Request Range Not Satisfiable',
42 | 417 : 'Expectation Failed',
43 | 422 : 'Unprocessable Entity',
44 | 423 : 'Locked',
45 | 424 : 'Failed Dependency',
46 | 500 : 'Internal Server Error',
47 | 501 : 'Not Implemented',
48 | 502 : 'Bad Gateway',
49 | 503 : 'Service Unavailable',
50 | 504 : 'Gateway Timeout',
51 | 505 : 'HTTP Version Not Supported',
52 | 507 : 'Insufficient Storage'
53 | };
54 |
55 | exports.HTTP_STATUS_MESSAGES = {};
56 | for (var code in exports.HTTP_STATUS_CODES)
57 | exports.HTTP_STATUS_MESSAGES[exports.HTTP_STATUS_CODES[code]] = parseInt(code);
58 |
59 | exports.STATUS_WITH_NO_ENTITY_BODY = function(status) { return (status >= 100 && status <= 199) || status == 204 || status == 304; };
60 |
61 | exports.responseForStatus = function(status, optMessage) {
62 | if (exports.HTTP_STATUS_CODES[status] === undefined)
63 | throw "Unknown status code";
64 |
65 | var message = exports.HTTP_STATUS_CODES[status];
66 |
67 | if (optMessage)
68 | message += ": " + optMessage;
69 |
70 | var body = (message+"\n").toByteString("UTF-8");
71 |
72 | return {
73 | status : status,
74 | headers : { "Content-Type" : "text/plain", "Content-Length" : String(body.length) },
75 | body : [body]
76 | };
77 | }
78 |
79 | exports.parseQuery = require("jack/querystring").parseQuery;
80 | exports.toQueryString = require("jack/querystring").toQueryString;
81 | exports.unescape = require("jack/querystring").unescape;
82 | exports.escape = require("jack/querystring").escape;
83 |
84 |
85 | var EOL = "\r\n";
86 |
87 | exports.parseMultipart = function(env, options) {
88 | options = options || {};
89 |
90 | var match, i, data;
91 | if (env['CONTENT_TYPE'] && (match = env['CONTENT_TYPE'].match(/^multipart\/form-data.*boundary=\"?([^\";,]+)\"?/m))) {
92 | var boundary = "--" + match[1],
93 |
94 | params = {},
95 | buf = "",
96 | contentLength = parseInt(env['CONTENT_LENGTH']),
97 | input = env['jsgi.input'],
98 |
99 | boundaryLength = boundary.length + EOL.length,
100 | bufsize = 16384;
101 |
102 | contentLength -= boundaryLength;
103 |
104 | var status = input.read(boundaryLength).decodeToString("UTF-8");
105 | if (status !== boundary + EOL)
106 | throw new Error("EOFError bad content body");
107 |
108 | var rx = new RegExp("(?:"+EOL+"+)?"+RegExp.escape(boundary)+"("+EOL+"|--)");
109 |
110 | while (true) {
111 | var head = null,
112 | body = new ByteIO(),
113 | filename = null,
114 | contentType = null,
115 | name = null,
116 | tempfile = null;
117 |
118 | while (!head || !rx.test(buf)) {
119 | if (!head && (i = buf.indexOf("\r\n\r\n")) > 0) {
120 | head = buf.substring(0, i+2);
121 | buf = buf.substring(i+4);
122 |
123 | match = head.match(/Content-Disposition:.* filename="?([^\";]*)"?/i);
124 | filename = match && match[1];
125 | match = head.match(/Content-Type: (.*)\r\n/i);
126 | contentType = match && match[1];
127 | match = head.match(/Content-Disposition:.* name="?([^\";]*)"?/i);
128 | name = match && match[1];
129 |
130 | if (filename) {
131 | // TODO: bypass String conversion for files, use Binary/ByteString directly
132 | if (options.nodisk) {
133 | tempfile = null;
134 | body = new ByteIO();
135 | } else {
136 | tempfile = "/tmp/jackupload-"+Math.round(Math.random()*100000000000000000).toString(16);
137 | body = FS.open(tempfile, "wb");
138 | }
139 | }
140 |
141 | continue;
142 | }
143 |
144 | // Save the read body part.
145 | if (head && (boundaryLength + 4 < buf.length)) {
146 | body.write(buf.slice(0, buf.length - (boundaryLength + 4)));
147 | buf = buf.slice(buf.length - (boundaryLength + 4));
148 | }
149 |
150 | var bytes = input.read(bufsize < contentLength ? bufsize : contentLength);
151 | if (!bytes)
152 | throw new Error("EOFError bad content body");
153 |
154 | var c = bytes.decodeToString("UTF-8");
155 |
156 | buf += c;
157 | contentLength -= bytes.length;
158 | }
159 |
160 | // Save the rest.
161 | if (match = buf.match(rx)) {
162 | body.write(buf.slice(0, match.index));
163 | buf = buf.slice(match.index + boundaryLength + 2);
164 |
165 | if (match[1] === "--")
166 | contentLength = -1;
167 | }
168 |
169 | if (filename === "") {
170 | data = null;
171 | }
172 | else if (filename || (!filename && contentType)) {
173 | body.close();
174 | //body.rewind();
175 | data = {
176 | "filename" : filename,
177 | "type" : contentType,
178 | "name" : name,
179 | "tempfile" : tempfile || body.toByteString("UTF-8"), // body
180 | "head" : head
181 | };
182 | if (filename) {
183 | // Take the basename of the upload's original filename.
184 | // This handles the full Windows paths given by Internet Explorer
185 | // (and perhaps other broken user agents) without affecting
186 | // those which give the lone filename.
187 | data.filename = filename.match(/^(?:.*[:\\\/])?(.*)/m)[1];
188 | }
189 | } else {
190 | data = body.decodeToString("UTF-8");
191 | }
192 |
193 | if (name) {
194 | if (/\[\]$/.test(name)) {
195 | params[name] = params[name] || [];
196 | params[name].push(data);
197 | } else {
198 | params[name] = data;
199 | }
200 | }
201 | if (!buf || contentLength == -1)
202 | break;
203 | }
204 |
205 | return params;
206 | }
207 |
208 | return null;
209 | }
210 |
211 |
--------------------------------------------------------------------------------
/lib/nitro.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Nitro, a Web Application micro-framework on top of JSGI.
3 | */
4 |
5 | /** @const */ exports.version = "0.9.1";
6 |
--------------------------------------------------------------------------------
/lib/nitro/cache.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview HTTP caching utilities.
3 | *
4 | * http://redbot.org/ (cache header check)
5 | * http://www.apps.ietf.org/rfc/rfc2616.html
6 | * http://tomayko.com/src/rack-cache/
7 | * http://www.xml.com/lpt/a/1642
8 | * http://guides.rubyonrails.org/caching_with_rails.html#conditional-get-support
9 | * http://fishbowl.pastiche.org/2002/10/21/http_conditional_get_for_rss_hackers/
10 | *
11 | */
12 |
13 | var memcache = require("google/appengine/api/memcache"),
14 | objects = require("ringo/utils/objects"),
15 | Response = require("nitro/response").Response;
16 |
17 | /**
18 | * Increase the seq parameter to invalidate all published etags.
19 | */
20 | exports.seq = 1;
21 |
22 | /**
23 | * Enable or disable the cache.
24 | */
25 | exports.enabled = true;
26 |
27 | /**
28 | * Cache the whole response.
29 | */
30 | exports.cacheLastMofified = function (env, lm, app) {
31 | if (!exports.enabled) return app(env);
32 |
33 | var etag = '"' + lm.getTime().toString() + ":" + exports.seq + '"';
34 |
35 | if (env["HTTP_IF_NONE_MATCH"] == etag) {
36 | // print("--not modified");
37 | return Response.notModified();
38 | } else {
39 | var mkey = "frg://" + env["nitro.original_path_info"] + "?" + env["QUERY_STRING"] + ":" + etag,
40 | headers = {
41 | "Cache-Control": "public, must-revalidate",
42 | "Last-Modified": lm.toGMTString(),
43 | "ETag": etag,
44 | "Nitro-Memcache-Key": mkey
45 | };
46 |
47 | if (memcache.get(mkey)) {
48 | // print("--- memcache hit");
49 | return { headers: headers };
50 | } else {
51 | // print("--- rendering");
52 | var response = app(env);
53 | response.headers = objects.merge(headers, response.headers);
54 | return response;
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * Cache a fragment.
61 | */
62 | exports.cacheLastMofifiedFragment = function (env, lm, path, app) {
63 | if (!exports.enabled) return app(env);
64 |
65 | var etag = lm.getTime().toString() + ":" + exports.seq,
66 | mkey = "frg://" + path + ":" + etag,
67 | fragment = memcache.get(mkey);
68 |
69 | if (fragment) {
70 | // print("--- memcache hit");
71 | return fragment;
72 | } else {
73 | // print("--- rendering");
74 | var fragment = app(env);
75 | memcache.set(mkey, fragment);
76 | return fragment;
77 | }
78 | }
79 |
80 | /**
81 | * Generate static response headers.
82 | */
83 | var staticResponseHeaders = exports.staticResponseHeaders = function (days) {
84 | if (exports.enabled) {
85 | var d = new Date(),
86 | seconds = (days || 30) * 86400000;
87 |
88 | d.setTime(d.getTime() + seconds);
89 |
90 | return {
91 | "Cache-Control": "public, max-age=" + seconds,
92 | "Expires": d.toGMTString()
93 | }
94 | } else {
95 | return {};
96 | }
97 | }
98 |
99 | /**
100 | * Generate a static response.
101 | */
102 | var staticResponse = exports.staticResponse = function (days, headers, data) {
103 | var response = {headers: headers || {}, data: data || {}};
104 |
105 | if (exports.enabled) {
106 | var d = new Date(),
107 | seconds = (days || 30) * 86400000;
108 |
109 | d.setTime(d.getTime() + seconds);
110 |
111 | response.headers["Cache-Control"] = "public, max-age=" + seconds;
112 | response.headers["Expires"] = d.toGMTString();
113 | }
114 |
115 | return response;
116 | }
117 |
118 | /**
119 | * A jsgi app that returns a static response.
120 | */
121 | exports.staticApp = function (env) {
122 | return staticResponse();
123 | }
124 |
125 | /**
126 | * Used by the deploy tool to prerender the action as a static file.
127 | */
128 | exports.staticApp.isStatic = true;
129 |
--------------------------------------------------------------------------------
/lib/nitro/middleware/api.js:
--------------------------------------------------------------------------------
1 | /** @fileoverview API middleware.*/
2 |
3 | /**
4 | * Wrap an API response.
5 | */
6 | var wrapApiResponse = exports.wrapApiResponse = function (response) {
7 | if (response.data) {
8 | response.headers = response.headers || {};
9 | response.headers["Content-Type"] = response.headers["Content-Type"] || "application/json";
10 | // response.headers["API-Version"] = "1.0";
11 | // response.body = [JSON.stringify(response.data).toByteString("UTF-8")];
12 | response.body = [JSON.stringify(response.data)];
13 | delete response.data;
14 | }
15 |
16 | return response;
17 | }
18 |
19 | /**
20 | * API middleware.
21 | *
22 | * This alternative to the Render middleware, implements a REST API that follows
23 | * the design of GData 2.0:
24 | * http://code.google.com/apis/gdata/docs/2.0/reference.html
25 | *
26 | * @param {Function} The upstream application.
27 | * @returns {Function} The wrapped JSGI app.
28 | */
29 | exports.API = exports.middleware = function (app) {
30 | return function (request) {
31 | // TODO: redirect api.myapp.com -> www.myapp.com/api
32 | // no need to handle ?alt, json is returned by default.
33 |
34 | return wrapApiResponse(app(request));
35 | }
36 | }
37 |
38 |
39 |
--------------------------------------------------------------------------------
/lib/nitro/middleware/dispatch.js:
--------------------------------------------------------------------------------
1 | var FS = require("fs");
2 |
3 | var Request = require("nitro/request").Request;
4 |
5 | /**
6 | * A middleware that selects an app from the root tree.
7 | * In essence it acts like a Unix shell (or like PHP ;-))
8 | * This is the *right thing* to do!
9 | *
10 | * Options:
11 | * dispatchRoot: the root directory for 'apps'.
12 | */
13 | var Dispatch = exports.Dispatch = exports.middleware = function (options) {
14 |
15 | if (!options) options = {};
16 |
17 | var root = FS.canonical(options.dispatchRoot || "src/root");
18 |
19 | var dispatch = function (request) {
20 | try {
21 | var script = require(root + request.scriptName);
22 | } catch (e) {
23 | if (FS.exists(root + request.scriptName + ".js")) {
24 | throw e;
25 | } else {
26 | return {status: 404, headers: {}, body: ["Action not found", e.toString()]};
27 | }
28 | }
29 |
30 | var app = getApp(script, request.method);
31 |
32 | if (app) {
33 | var response = app(request);
34 | return response ? setDefaults(response) : {status: 200, headers: {}, body: [], data: {}};
35 | } else {
36 | return {status: 405, headers: {}, body: ["'" + request.scriptName + "' does not respond to HTTP method '" + request.method + "'"]};
37 | }
38 | }
39 |
40 | return function (request) {
41 | new Request(request);
42 | request.dispatch = dispatch;
43 | return dispatch(request);
44 | }
45 |
46 | }
47 |
48 | var getApp = function (script, method) {
49 | if ((method == "HEAD") && (!script[method])) {
50 | // If the script has no HEAD app use the GET app.
51 | return script.GET;
52 | }
53 |
54 | return script[method];
55 | }
56 |
57 | var setDefaults = function (response) {
58 | if (!response.status) response.status = 200;
59 | if (!response.headers) response.headers = {};
60 | if (!response.body) response.body = [];
61 |
62 | return response;
63 | }
64 |
--------------------------------------------------------------------------------
/lib/nitro/middleware/errors.js:
--------------------------------------------------------------------------------
1 | var HTTP_STATUS_CODES = require("jack/utils").HTTP_STATUS_CODES;
2 |
3 | /**
4 | * Catches 4XX and 5XX errors from upstream.
5 | * Renders the error using the provided template.
6 | */
7 | exports.Errors = exports.middleware = function (app, options) {
8 |
9 | if (!options) options = {};
10 |
11 | var notFoundTemplatePath = options.notFoundTemplatePath || "/notfound.html",
12 | errorTemplatePath = options.errorTemplatePath || "/error.html";
13 |
14 | return function (request) {
15 | var response;
16 |
17 | try {
18 | response = app(request);
19 | } catch (e) {
20 | var backtrace = String((e.rhinoException && e.rhinoException.printStackTrace()) || (e.name + ": " + e.message));
21 | var msg = e.rhinoException ? " : " + e.rhinoException.getScriptStackTrace() : "";
22 | response = {status: 500, headers: {}, body: [e.toString() + msg], trace: e.rhinoException ? e.rhinoException.getScriptStackTrace() : "" };
23 | }
24 |
25 | var status = parseInt(response.status, 10);
26 |
27 | if (status >= 400) {
28 | response.data = {
29 | status: status,
30 | statusString: HTTP_STATUS_CODES[status],
31 | path: request.pathTranslated,
32 | error: response.body.join(" "),
33 | trace: response.trace
34 | }
35 |
36 | if (status >= 500) print(response.body.join("\n"));
37 |
38 | try {
39 | if (status < 500) {
40 | try {
41 | response.body = [request.render(notFoundTemplatePath, response.data)];
42 | } catch (e) {
43 | response.body = [request.render(errorTemplatePath, response.data)];
44 | }
45 | } else {
46 | response.body = [request.render(errorTemplatePath, response.data)];
47 | }
48 | } catch (e) {
49 | response.body = [response.data.error];
50 | }
51 | }
52 |
53 | return response;
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/lib/nitro/middleware/memcache.js:
--------------------------------------------------------------------------------
1 | var MEMCACHE = require("google/appengine/api/memcache"),
2 | CACHE = require("nitro/cache");
3 |
4 | /**
5 | * Full response memcache middleware.
6 | */
7 | exports.MemCache = exports.middleware = function (app, options) {
8 | if (options && options.seq) CACHE.seq = options.seq;
9 |
10 | return function (request) {
11 | var response = app(request),
12 | key = response.headers["Nitro-Memcache-Key"];
13 |
14 | delete response.headers["Nitro-Memcache-Key"];
15 |
16 | if (key) {
17 | // print("--- key: " + key);
18 | if (response.body.length > 0) {
19 | // print("--- rendered, storing to memcache");
20 | MEMCACHE.set(key, response.body[0]);
21 | } else {
22 | // print("--- serving from memcache");
23 | var body = MEMCACHE.get(key);
24 | if (body) {
25 | response.body = [body.toString()];
26 | }
27 | }
28 | }
29 |
30 | return response;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/nitro/middleware/path.js:
--------------------------------------------------------------------------------
1 | var MIME_TYPES = require("jack/mime").MIME_TYPES;
2 |
3 | var FS = require("fs");
4 |
5 | // Custom version of listTree that avoids .isLink()
6 | var listTree = function (path) {
7 | var paths = [""];
8 |
9 | FS.path(path).list(path).forEach(function (child) {
10 | var fullPath = FS.join(path, child);
11 | if (FS.isDirectory(fullPath)) {
12 | paths.push.apply(paths, listTree(fullPath).map(function(p) {
13 | return FS.join(child, p).replace(/\\/, "/"); // windows fix!
14 | }));
15 | } else {
16 | paths.push(child)
17 | }
18 | });
19 |
20 | return paths;
21 | };
22 |
23 | // Compute a hash of all the scripts in dispatchRoot.
24 | var computeSitemap = function (root) {
25 | var sitemap = {};
26 |
27 | listTree(root).forEach(function (f) {
28 | if (f.match(/\.js$/)) {
29 | sitemap["/" + f.replace(/\.js$/, "")] = true;
30 | }
31 | });
32 |
33 | return sitemap;
34 | }
35 |
36 | /**
37 | * Normalize the request path, extract scriptName, pathInfo, pathTranslated.
38 | *
39 | * .scriptName: The initial portion of the request URL's "path" that corresponds
40 | * to the Application object, so that the Application knows its virtual
41 | * "location". This MAY be an empty string, if the Application corresponds to
42 | * the "root" of the Server. Restriction: if non-empty `scriptName` MUST start
43 | * with "/", MUST NOT end with "/" and MUST NOT be decoded.
44 | *
45 | * .pathInfo: The remainder of the request URL's "path", designating the virtual
46 | * "location" of the Request's target within the Application. This may be an
47 | * empty string, if the request URL targets the Application root and does not
48 | * have a trailing slash. Restriction: if non-empty `pathInfo` MUST start with
49 | * "/" and MUST NOT be decoded.
50 | */
51 | exports.Path = exports.middleware = function (app, options) {
52 | options = options || {};
53 |
54 | if (options.sitemap) {
55 | var sitemap = options.sitemap;
56 | } else {
57 | var sitemap = computeSitemap(options.scriptRoot || "src/root");
58 | }
59 |
60 | return function (request) {
61 | var scriptName = request.pathInfo; // no need for shallow copy.
62 |
63 | var ext = ".html";
64 |
65 | if (scriptName == "/") { // handle /
66 | scriptName = "/index";
67 | request.contentType = "text/html";
68 | } else {
69 | if (/\/$/.test(scriptName)) { // remove trailing / -> text/html
70 | request.pathInfo = scriptName.replace(/\/$/, "");
71 | request.contentType = "text/html";
72 | } else {
73 | var idx = scriptName.lastIndexOf("."),
74 | ext = scriptName.slice(idx),
75 | mime = MIME_TYPES[ext];
76 | if (mime) { // uri contains valid mime type.
77 | request.contentType = mime
78 | request.pathInfo = scriptName = scriptName.slice(0, idx);
79 | } else { // set default mime type.
80 | ext = ".html";
81 | request.contentType = "text/html";
82 | }
83 | }
84 | }
85 |
86 | // TODO: add dispatchRoot as prefix.
87 | request.pathTranslated = request.scriptName + request.pathInfo + ext;
88 |
89 | idx = 99999;
90 |
91 | while (!sitemap[scriptName]) {
92 | idx = scriptName.lastIndexOf("/");
93 | if (idx == 0) {
94 | return {
95 | status: 404,
96 | headers: {},
97 | body: []
98 | }
99 | // no '/'-based nice urls!
100 | // scriptName = "/index";
101 | // break;
102 | }
103 | scriptName = scriptName.slice(0, idx);
104 | }
105 |
106 | request.pathInfo = request.pathInfo.slice(idx);
107 | request.scriptName = scriptName;
108 |
109 | return app(request);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/lib/nitro/middleware/render.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Render middleware.
3 | *
4 | * Inspects the headers of the upstream response. If the response includes a data object
5 | * it evaluates the template that corresponds to the given path info and sets the
6 | * response body to the rendered template.
7 | *
8 | * Options:
9 | * templateEngine = the template engine to use (default: the included Normal Template engine)
10 | * templateRoot = the root directory for templates
11 | */
12 | exports.Render = exports.middleware = function (app, options) {
13 | if (!options) options = {};
14 |
15 | var templateEngine = new (options.templateEngine || require("nitro/template").TemplateEngine)(options);
16 |
17 | var render = function (templatePath, data) {
18 | if (options.cacheTemplates) {
19 | return templateEngine.get(templatePath)(data);
20 | } else {
21 | return templateEngine.compileTemplate(templatePath)(data);
22 | }
23 | }
24 |
25 | return function (request) {
26 | request.render = render;
27 |
28 | var response = app(request);
29 |
30 | if (response.data) {
31 | var templatePath = response.data.TEMPLATE_PATH || request.scriptName + "." + request.pathTranslated.split(".").pop();
32 | response.body = [render(templatePath, response.data)];
33 | }
34 |
35 | return response;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/nitro/middleware/setup.js:
--------------------------------------------------------------------------------
1 | var ContentLength = require("jack/contentlength").ContentLength,
2 | ContentType = require("jack/contenttype").ContentType,
3 | MethodOverride = require("jack/methodoverride").MethodOverride,
4 | Head = require("jack/head").Head;
5 |
6 | /**
7 | * Helper middleware that applies a standard pipeline of middleware.
8 | */
9 | exports.Setup = exports.middleware = function (app) {
10 | var upstream = ContentLength(ContentType(Head(MethodOverride(app))));
11 |
12 | return function (request) {
13 | return upstream(request);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/lib/nitro/request.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Encapsulates an HTTP Request.
3 | * The Nitro request object is an extension of the standard
4 | * JSGI Request object.
5 | */
6 |
7 | exports.Request = require("ringo/webapp/request").Request;
8 |
--------------------------------------------------------------------------------
/lib/nitro/response.js:
--------------------------------------------------------------------------------
1 | var Response = exports.Response = require("ringo/webapp/response").Response;
2 |
3 | /**
4 | * Constructs a redirect (30x).
5 | */
6 | Response.redirect = function (location, status) {
7 | return {
8 | status: status || 302,
9 | headers: {"Location": location},
10 | body: ['Go to ' + location + " "]
11 | }
12 | }
13 |
14 | /**
15 | * Constructs a success (200) response.
16 | */
17 | Response.ok = function () {
18 | return {status: 200, headers: {}, body: []}
19 | };
20 |
21 | /**
22 | * Constructs a created (201) response.
23 | */
24 | Response.created = function (uri) {
25 | return {status: 201, headers: {"Location": uri}, body: []}
26 | };
27 |
28 | /**
29 | * Constructs a plain html response.
30 | */
31 | Response.html = function (html, charset) {
32 | charset = charset || "utf-8";
33 | return {
34 | status: 200,
35 | headers: {"Content-Type": "text/html; charset=" + charset},
36 | body: [html.toByteString(charset)]
37 | }
38 | }
39 |
40 | /**
41 | * Dumps the data object as a JSON string.
42 | */
43 | Response.json = function (data) {
44 | if (typeof data !== "string")
45 | data = JSON.stringify(data);
46 | return {
47 | status: data.status || 200,
48 | headers: {"Content-Type": "application/json"},
49 | body: [data.toByteString("utf-8")]
50 | }
51 | }
52 |
53 | /**
54 | * Constructs a JSONP response.
55 | * http://en.wikipedia.org/wiki/JSON#JSONP
56 | */
57 | Response.jsonp = function (data, callback) {
58 | if (typeof data !== "string")
59 | data = JSON.stringify(data);
60 | return {
61 | status: data.status || 200,
62 | headers: {"Content-Type": "application/javascript"},
63 | body: [(callback + "(" + data + ")").toByteString("utf-8")]
64 | }
65 | }
66 |
67 | /**
68 | * Constructs a chunked response.
69 | * Useful for streaming/comet applications.
70 | */
71 | Response.chunked = function (func) {
72 | // FIXME: make this RingoJS compatible!
73 | return new Response(200, { "Transfer-Encoding": "chunked" }).finish(func);
74 | }
75 |
76 | /**
77 | * a 304 (Not modified) response.
78 | */
79 | Response.notModified = function () {
80 | return {status: 304, headers: {}, body: []};
81 | }
82 |
83 | /**
84 | * A 400 (Bad request) response.
85 | */
86 | Response.badRequest = function (msg) {
87 | return {status: 400, headers: {}, body: [msg || "Bad request"]};
88 | }
89 |
90 | /**
91 | * A 401 (Unauthorized) response.
92 | */
93 | Response.unauthorized = function (msg) {
94 | return {status: 401, headers: {}, body: [msg || "Unauthorized"]};
95 | }
96 |
97 | /**
98 | * A 403 (Forbidden) response.
99 | */
100 | Response.forbidden = function (msg) {
101 | return {status: 403, headers: {}, body: [msg || "Forbidden"]};
102 | }
103 |
104 | /**
105 | * A 404 (Not found) response.
106 | */
107 | Response.notFound = function (msg) {
108 | return {status: 404, headers: {}, body: [msg || "Not found"]};
109 | }
110 |
111 | /**
112 | * A 405 (Method not allowed) response.
113 | */
114 | Response.methodNotAllowed = function (msg) {
115 | return {status: 405, headers: {}, body: [msg || "Method not allowed"]};
116 | }
117 |
118 | /**
119 | * A 406 (Not acceptable) response.
120 | */
121 | Response.notAcceptable = function (msg) {
122 | return {status: 406, headers: {}, body: [msg || "Not acceptable"]};
123 | }
124 |
125 | /**
126 | * A 409 (Conflict) response.
127 | */
128 | Response.conflict = function (msg) {
129 | return {status: 409, headers: {}, body: [msg || "Conflict"]};
130 | }
131 |
--------------------------------------------------------------------------------
/lib/nitro/session.js:
--------------------------------------------------------------------------------
1 | exports.Session = require("ringo/webapp/request").Session;
2 |
--------------------------------------------------------------------------------
/lib/nitro/template.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview A template engine wraps standard JavaScript templating solutions
3 | * for use with Nitro. A normal-template based engine is provided by default.
4 | */
5 |
6 | var fs = require("fs"),
7 | objects = require("ringo/utils/objects"),
8 | normal = require("normal-template"),
9 | tpp = require("normal-template/tpp");
10 |
11 | /**
12 | * The default Nitro Template Engine is powered by Normal Template.
13 | * http://www.github.com/gmosx/normal-templateEngine
14 | *
15 | * Alternative template engines can be integrated with Nitro by creating
16 | * wrapper modules with this interface.
17 | *
18 | * Options:
19 | * - templateRoot
20 | * - compileOptions
21 | *
22 | * @constructor
23 | */
24 | var TemplateEngine = exports.TemplateEngine = function (options) {
25 | if (!options) options = {};
26 |
27 | // Root directory for template files.
28 | var templateRoot = this.templateRoot = options.templateRoot || "src/templates";
29 |
30 | // The options to pass to the template compiler.
31 | // By default html escaping is applied for extra security.
32 | this.compileOptions = options.compileOptions || {filters: {defaultfilter: normal.filters.html}};
33 |
34 | // Cache for the compiled templates.
35 | this.cache = {};
36 |
37 | var loadTemplate = this.loadTemplate = function (path) {
38 | path = fs.join(templateRoot, path);
39 |
40 | // FIXME: fs.exists(path) does not work on GAE?
41 | // try {
42 | return fs.read(path, {charset: "UTF-8"});
43 | // } catch (e) {
44 | // return false;
45 | // }
46 | }
47 |
48 | // Preprocess the template. Apply includes and render the optional
49 | // super-template (aka meta-template).
50 | // HINT: function implemented inline to allow for passing loadTemplate
51 | // to expandIncludes.
52 | this.preprocessTemplate = function (src) {
53 | var st, stSrc, stPath;
54 |
55 | if (stPath = tpp.getTemplatePath(src)) {
56 | // The template defines values for a super-template.
57 | if (stSrc = loadTemplate(stPath)) {
58 | stSrc = tpp.expandIncludes(stSrc, loadTemplate);
59 | st = normal.compile(stSrc);
60 | src = tpp.expandIncludes(src, loadTemplate);
61 | var data = tpp.extractData(src);
62 | src = st(objects.merge(data, options.tppData || {}));
63 | return src;
64 | } else {
65 | throw new Error("Template '" + stPath + "' not found");
66 | }
67 | } else {
68 | src = tpp.expandIncludes(src, loadTemplate);
69 | return src;
70 | }
71 | }
72 |
73 | // Compile the template.
74 | this.compileTemplate = function (path) {
75 | var src = loadTemplate(path);
76 |
77 | if (src) {
78 | return normal.compile(this.preprocessTemplate(src), this.compileOptions);
79 | } else {
80 | throw new Error("Template '" + path + "' not found");
81 | }
82 | }
83 | }
84 |
85 | /**
86 | * Load, preprocess and compile a template.
87 | */
88 | TemplateEngine.prototype.get = function (path) {
89 | var template = this.cache[path];
90 |
91 | if (!template) {
92 | this.cache[path] = template = this.compileTemplate(path);
93 | }
94 |
95 | return template;
96 | }
97 |
--------------------------------------------------------------------------------
/lib/nitro/utils/atom.js:
--------------------------------------------------------------------------------
1 | var escapeHTML = require("narwhal/html").escape;
2 |
3 | /**
4 | * Atom codec.
5 | * TODO: Implement with E4X?
6 | */
7 |
8 | // Serialize a single object.
9 | var encodeObject = function (obj, options) {
10 | return '\
11 | \n\
12 | ' + obj.title + ' \n\
13 | \n\
14 | ' + obj.key() + ' \n\
15 | ' + obj.created.toISOString() + ' \n\
16 | ' + obj.created.toISOString() + ' \n\
17 | ' + escapeHTML(obj.content) + ' \n\
18 | ';
19 | }
20 |
21 | /**
22 | * Serialize an object or (typically) a collection of objects to an Atom string.
23 | */
24 | exports.encode = function (obj, options) {
25 | if (Array.isArray(obj)) {
26 | var feed = '\
27 | \n\
28 | ' + options.self + ' \n\
29 | ' + options.title + ' \n\
30 | ' + options.updated.toISOString() + ' \n\
31 | \n\
32 | \n';
33 |
34 | for (var i = 0; i < obj.length; i++) {
35 | feed += encodeObject(obj[i], options);
36 | }
37 |
38 | feed += '\
39 | ';
40 | return feed;
41 | } else {
42 | return encodeObject(obj, options);
43 | }
44 | }
45 |
46 | /**
47 | * Deserialize an object from an Atom string.
48 | */
49 | exports.decode = function (str) {
50 | }
51 |
52 | /**
53 | * Generate an atom feed JSGI response.
54 | */
55 | exports.feedResponse = function (request, items, options) {
56 | var base = "http://" + request.host + (request.port == "80" ? "" : ":" + request.port);
57 |
58 | if (!options) options = {};
59 |
60 | options.title = options.title || "Feed";
61 | options.self = options.self || base + request.pathTranslated;
62 | options.base = options.base || options.self.replace(/\.atom$/, "");
63 | options.updated = options.updated || items[0].updated || items[0].created;
64 |
65 | return {status: 200, headers: {"Content-Type": "text/atom"}, body: [exports.encode(items, options)]};
66 | }
67 |
--------------------------------------------------------------------------------
/lib/nitro/utils/fileupload.js:
--------------------------------------------------------------------------------
1 | // TODO: Convert to Ringo or deprecate in favor of blobstore.
2 |
3 | var JDiskFileItemFactory = Packages.org.apache.commons.fileupload.disk.DiskFileItemFactory,
4 | JServletFileUpload = Packages.org.apache.commons.fileupload.servlet.ServletFileUpload;
5 |
6 | var ByteString = require("binary").ByteString,
7 | IO = require("io-engine").IO;
8 |
9 | var jupload = new JServletFileUpload();
10 |
11 | /**
12 | * Parses a multipart request in memory using Apache Commons FileUpload.
13 | * Compatible with Google App Engine (no temp file created).
14 | *
15 | * The following jars are required:
16 | * - commons-fileupload-1.2.X.jar
17 | * - commons-io.1.X.jar
18 | *
19 | * http://commons.apache.org/fileupload/index.html
20 | */
21 | exports.parseMultipart = function(env) {
22 | var params = env["jack.request.params_hash"];
23 | if (!params) {
24 | params = env["jack.request.params_hash"] = {};
25 |
26 | var jrequest = env["jack.servlet_request"];
27 | var iterator = jupload.getItemIterator(jrequest);
28 |
29 | while (iterator.hasNext()) {
30 | var item = iterator.next(),
31 | field = String(item.getFieldName()),
32 | io = new IO(item.openStream());
33 |
34 | if (item.isFormField()) {
35 | params[field] = io.read().toString("utf8");
36 | } else {
37 | var data = io.read();
38 | params[field] = {
39 | name: String(item.getName()),
40 | type: String(item.contentType),
41 | size: data.length,
42 | data: data
43 | }
44 | }
45 | }
46 | }
47 |
48 | return params;
49 | }
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nitro",
3 | "description": "A Web Application Framework",
4 | "keywords": ["web", "framework", "jsgi"],
5 | "author": "George Moschovitis",
6 | "email": "george.moschovitis@gmail.com",
7 | "dependencies": [],
8 | "contributors": [
9 | "Kris Kowal (http://askawizard.blogspot.com/)"
10 | ],
11 | "lib": "lib",
12 | "main": "examples/config.js",
13 | "lean": {
14 | "include": [
15 | "lib/**/*",
16 | "package.json"
17 | ]
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/test/nitro/middleware/dispatch.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert");
2 |
3 | var DISPATCH = require("../../../lib/nitro/middleware/dispatch");
4 |
5 | /*
6 | exports.testSetDefaults = function () {
7 | var response = DISPATCH.setDefaults({});
8 | assert.deepEqual(response, {status: 200, headers: {}, body: []});
9 | }
10 | */
11 |
12 | /*
13 | exports.testRealPath = function() {
14 | var sitemap = {
15 | "/index": {title: "Home"},
16 | "/blog": {title: "Blog"},
17 | "/blog/article": {title: "Article"}
18 | }
19 |
20 | var env = { "PATH_INFO": "/index.html" }
21 | assert.isEqual("/index", realPath(env, sitemap));
22 | assert.isEqual(undefined, env.args);
23 |
24 | var env = { "PATH_INFO": "/blog/2312342/this-is-a-nice-article" }
25 | assert.isEqual("/blog", realPath(env, sitemap));
26 | assert.isEqual(2, env.args.length);
27 | assert.isEqual("2312342", env.args[0]);
28 | assert.isEqual("this-is-a-nice-article", env.args[1]);
29 |
30 | var env = { "PATH_INFO": "/not/in/map.html" }
31 | assert.isEqual("/not/in/map", realPath(env, sitemap));
32 | }
33 | */
34 |
--------------------------------------------------------------------------------
/test/nitro/middleware/path.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gmosx/nitro-js/520b75b3d9d94e820a572a705a182a11c7288e52/test/nitro/middleware/path.js
--------------------------------------------------------------------------------
/test/run.js:
--------------------------------------------------------------------------------
1 | exports.testDispatch = require("./nitro/middleware/dispatch");
2 |
3 | if (module === require.main) {
4 | require("test").run(exports);
5 | }
6 |
--------------------------------------------------------------------------------