├── .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 | * 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 | --------------------------------------------------------------------------------