/* Copyright (c) 2010 Tim Caswell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var sys = require('sys'); var fs = require('fs'); var path = require('path'); var http = require('http'); var url_parse = require("url").parse; // Used as a simple, convient 404 handler. function notFound(req, res, message) { message = (message || "Not Found\n") + ""; res.writeHead(404, { "Content-Type": "text/plain", "Content-Length": message.length }); if (req.method !== "HEAD") res.write(message); res.end(); } // Modifies req and res to call logger with a log line on each res.end // Think of it as "middleware" function logify(req, res, logger) { var end = res.end; res.end = function () { // Common Log Format (mostly) logger((req.socket && req.socket.remoteAddress) + " - - [" + (new Date()).toUTCString() + "]" + " \"" + req.method + " " + req.url + " HTTP/" + req.httpVersionMajor + "." + req.httpVersionMinor + "\" " + res.statusCode + " - \"" + (req.headers['referer'] || "") + "\" \"" + (req.headers["user-agent"] ? req.headers["user-agent"].split(' ')[0] : '') + "\""); return end.apply(this, arguments); } var writeHead = res.writeHead; res.writeHead = function (code) { res.statusCode = code; return writeHead.apply(this, arguments); } } exports.getServer = function getServer(logger) { logger = logger || sys.puts; var routes = []; // Adds a route the the current server function addRoute(method, pattern, handler, format) { if (typeof pattern === 'string') { pattern = new RegExp("^" + pattern + "$"); } var route = { method: method, pattern: pattern, handler: handler }; if (format !== undefined) { route.format = format; } routes.push(route); } // The four verbs are wrappers around addRoute function get(pattern, handler) { return addRoute("GET", pattern, handler); } function post(pattern, handler, format) { return addRoute("POST", pattern, handler, format); } function put(pattern, handler, format) { return addRoute("PUT", pattern, handler, format); } function del(pattern, handler) { return addRoute("DELETE", pattern, handler); } function head(pattern, handler) { return addRoute("HEAD", pattern, handler); } // This is a meta pattern that expands to a common RESTful mapping function resource(name, controller, format) { get(new RegExp('^/' + name + '$'), controller.index); get(new RegExp('^/' + name + '/([^/]+)$'), controller.show); post(new RegExp('^/' + name + '$'), controller.create, format); put(new RegExp('^/' + name + '/([^/]+)$'), controller.update, format); del(new RegExp('^/' + name + '/([^/]+)$'), controller.destroy); }; function resourceController(name, data, on_change) { data = data || []; on_change = on_change || function () {}; return { index: function (req, res) { res.simpleJson(200, {content: data, self: '/' + name}); }, show: function (req, res, id) { var item = data[id]; if (item) { res.simpleJson(200, {content: item, self: '/' + name + '/' + id}); } else { res.notFound(); } }, create: function (req, res) { req.jsonBody(function (json) { var item, id, url; item = json && json.content; if (!item) { res.notFound(); } else { data.push(item); id = data.length - 1; on_change(id); url = "/" + name + "/" + id; res.simpleJson(201, {content: item, self: url}, [["Location", url]]); } }); }, update: function (req, res, id) { req.jsonBody(function (json) { var item = json && json.content; if (!item) { res.notFound(); } else { data[id] = item; on_change(id); res.simpleJson(200, {content: item, self: "/" + name + "/" + id}); } }); }, destroy: function (req, res, id) { delete data[id]; on_change(id); res.simpleJson(200, "200 Destroyed"); } }; }; // Create the http server object var server = http.createServer(function (req, res) { // Enable logging on all requests using common-logger style logify(req, res, logger); var uri, path; // Performs an HTTP 302 redirect res.redirect = function redirect(location) { res.writeHead(302, {"Location": location}); res.end(); } // Performs an internal redirect res.innerRedirect = function innerRedirect(location) { logger("Internal Redirect: " + req.url + " -> " + location); req.url = location; doRoute(); } function simpleResponse(code, body, content_type, extra_headers) { res.writeHead(code, (extra_headers || []).concat( [ ["Content-Type", content_type], ["Content-Length", Buffer.byteLength(body, 'utf8')] ])); if (req.method !== "HEAD") res.write(body, 'utf8'); res.end(); } res.simpleText = function (code, body, extra_headers) { simpleResponse(code, body, "text/plain", extra_headers); }; res.simpleHtml = function (code, body, extra_headers) { simpleResponse(code, body, "text/html", extra_headers); }; res.simpleJson = function (code, json, extra_headers) { simpleResponse(code, JSON.stringify(json), "application/json", extra_headers); }; res.notFound = function (message) { notFound(req, res, message); }; res.onlyHead = function (code, extra_headers) { res.writeHead(code, (extra_headers || []).concat( [["Content-Type", content_type]])); res.end(); } function doRoute() { uri = url_parse(req.url); path = uri.pathname; for (var i = 0, l = routes.length; i < l; i += 1) { var route = routes[i]; if (req.method === route.method) { var match = path.match(route.pattern); if (match && match[0].length > 0) { match.shift(); match = match.map(function (part) { return part ? unescape(part) : part; }); match.unshift(res); match.unshift(req); if (route.format !== undefined) { var body = ""; req.setEncoding('utf8'); req.addListener('data', function (chunk) { body += chunk; }); req.addListener('end', function () { if (route.format === 'json') { try { body = JSON.parse(unescape(body)); } catch(e) { body = null; } } match.push(body); route.handler.apply(null, match); }); return; } var result = route.handler.apply(null, match); switch (typeof result) { case "string": res.simpleHtml(200, result); break; case "object": res.simpleJson(200, result); break; } return; } } } notFound(req, res); } doRoute(); }); function listen(port, host, callback) { port = port || 8080; if (typeof host === 'undefined' || host == '*') host = null; server.listen(port, host, callback); if (typeof port === 'number') { logger("node-router server instance at http://" + (host || '*') + ":" + port + "/"); } else { logger("node-router server instance at unix:" + port); } } function end() { return server.end(); } // Return a handle to the public facing functions from this closure as the // server object. return { get: get, post: post, put: put, del: del, resource: resource, resourceController: resourceController, listen: listen, end: end }; } exports.staticHandler = function (filename) { var body, headers; var content_type = mime.getMime(filename) var encoding = (content_type.slice(0,4) === "text" ? "utf8" : "binary"); function loadResponseData(req, res, callback) { if (body && headers) { callback(); return; } fs.readFile(filename, encoding, function (err, data) { if (err) { notFound(req, res, "Cannot find file: " + filename); return; } body = data; headers = [ [ "Content-Type" , content_type ], [ "Content-Length" , body.length ] ]; headers.push(["Cache-Control", "public"]); callback(); }); } return function (req, res) { loadResponseData(req, res, function () { res.writeHead(200, headers); if (req.method !== "HEAD") res.write(body, encoding); res.end(); }); }; }; exports.staticDirHandler = function(root, prefix) { function loadResponseData(req, res, filename, callback) { var content_type = mime.getMime(filename); var encoding = (content_type.slice(0,4) === "text" ? "utf8" : "binary"); fs.readFile(filename, encoding, function(err, data) { if(err) { notFound(req, res, "Cannot find file: " + filename); return; } var headers = [ [ "Content-Type" , content_type ], [ "Content-Length" , data.length ], [ "Cache-Control" , "public" ] ]; callback(headers, data, encoding); }); } return function (req, res) { // trim off any query/anchor stuff var filename = req.url.replace(/[\?|#].*$/, ''); if (prefix) filename = filename.replace(new RegExp('^'+prefix), ''); // make sure nobody can explore our local filesystem filename = path.join(root, filename.replace(/\.\.+/g, '.')); if (filename == root) filename = path.join(root, 'index.html'); loadResponseData(req, res, filename, function(headers, body, encoding) { res.writeHead(200, headers); if (req.method !== "HEAD") res.write(body, encoding); res.end(); }); }; }; // Mini mime module for static file serving var DEFAULT_MIME = 'application/octet-stream'; var mime = exports.mime = { getMime: function getMime(path) { var index = path.lastIndexOf("."); if (index < 0) { return DEFAULT_MIME; } return mime.TYPES[path.substring(index).toLowerCase()] || DEFAULT_MIME; }, TYPES : { ".3gp" : "video/3gpp", ".a" : "application/octet-stream", ".ai" : "application/postscript", ".aif" : "audio/x-aiff", ".aiff" : "audio/x-aiff", ".asc" : "application/pgp-signature", ".asf" : "video/x-ms-asf", ".asm" : "text/x-asm", ".asx" : "video/x-ms-asf", ".atom" : "application/atom+xml", ".au" : "audio/basic", ".avi" : "video/x-msvideo", ".bat" : "application/x-msdownload", ".bin" : "application/octet-stream", ".bmp" : "image/bmp", ".bz2" : "application/x-bzip2", ".c" : "text/x-c", ".cab" : "application/vnd.ms-cab-compressed", ".cc" : "text/x-c", ".chm" : "application/vnd.ms-htmlhelp", ".class" : "application/octet-stream", ".com" : "application/x-msdownload", ".conf" : "text/plain", ".cpp" : "text/x-c", ".crt" : "application/x-x509-ca-cert", ".css" : "text/css", ".csv" : "text/csv", ".cxx" : "text/x-c", ".deb" : "application/x-debian-package", ".der" : "application/x-x509-ca-cert", ".diff" : "text/x-diff", ".djv" : "image/vnd.djvu", ".djvu" : "image/vnd.djvu", ".dll" : "application/x-msdownload", ".dmg" : "application/octet-stream", ".doc" : "application/msword", ".dot" : "application/msword", ".dtd" : "application/xml-dtd", ".dvi" : "application/x-dvi", ".ear" : "application/java-archive", ".eml" : "message/rfc822", ".eps" : "application/postscript", ".exe" : "application/x-msdownload", ".f" : "text/x-fortran", ".f77" : "text/x-fortran", ".f90" : "text/x-fortran", ".flv" : "video/x-flv", ".for" : "text/x-fortran", ".gem" : "application/octet-stream", ".gemspec" : "text/x-script.ruby", ".gif" : "image/gif", ".gz" : "application/x-gzip", ".h" : "text/x-c", ".hh" : "text/x-c", ".htm" : "text/html", ".html" : "text/html", ".ico" : "image/vnd.microsoft.icon", ".ics" : "text/calendar", ".ifb" : "text/calendar", ".iso" : "application/octet-stream", ".jar" : "application/java-archive", ".java" : "text/x-java-source", ".jnlp" : "application/x-java-jnlp-file", ".jpeg" : "image/jpeg", ".jpg" : "image/jpeg", ".js" : "application/javascript", ".json" : "application/json", ".log" : "text/plain", ".m3u" : "audio/x-mpegurl", ".m4v" : "video/mp4", ".man" : "text/troff", ".mathml" : "application/mathml+xml", ".mbox" : "application/mbox", ".mdoc" : "text/troff", ".me" : "text/troff", ".mid" : "audio/midi", ".midi" : "audio/midi", ".mime" : "message/rfc822", ".mml" : "application/mathml+xml", ".mng" : "video/x-mng", ".mov" : "video/quicktime", ".mp3" : "audio/mpeg", ".mp4" : "video/mp4", ".mp4v" : "video/mp4", ".mpeg" : "video/mpeg", ".mpg" : "video/mpeg", ".ms" : "text/troff", ".msi" : "application/x-msdownload", ".odp" : "application/vnd.oasis.opendocument.presentation", ".ods" : "application/vnd.oasis.opendocument.spreadsheet", ".odt" : "application/vnd.oasis.opendocument.text", ".ogg" : "application/ogg", ".p" : "text/x-pascal", ".pas" : "text/x-pascal", ".pbm" : "image/x-portable-bitmap", ".pdf" : "application/pdf", ".pem" : "application/x-x509-ca-cert", ".pgm" : "image/x-portable-graymap", ".pgp" : "application/pgp-encrypted", ".pkg" : "application/octet-stream", ".pl" : "text/x-script.perl", ".pm" : "text/x-script.perl-module", ".png" : "image/png", ".pnm" : "image/x-portable-anymap", ".ppm" : "image/x-portable-pixmap", ".pps" : "application/vnd.ms-powerpoint", ".ppt" : "application/vnd.ms-powerpoint", ".ps" : "application/postscript", ".psd" : "image/vnd.adobe.photoshop", ".py" : "text/x-script.python", ".qt" : "video/quicktime", ".ra" : "audio/x-pn-realaudio", ".rake" : "text/x-script.ruby", ".ram" : "audio/x-pn-realaudio", ".rar" : "application/x-rar-compressed", ".rb" : "text/x-script.ruby", ".rdf" : "application/rdf+xml", ".roff" : "text/troff", ".rpm" : "application/x-redhat-package-manager", ".rss" : "application/rss+xml", ".rtf" : "application/rtf", ".ru" : "text/x-script.ruby", ".s" : "text/x-asm", ".sgm" : "text/sgml", ".sgml" : "text/sgml", ".sh" : "application/x-sh", ".sig" : "application/pgp-signature", ".snd" : "audio/basic", ".so" : "application/octet-stream", ".svg" : "image/svg+xml", ".svgz" : "image/svg+xml", ".swf" : "application/x-shockwave-flash", ".t" : "text/troff", ".tar" : "application/x-tar", ".tbz" : "application/x-bzip-compressed-tar", ".tci" : "application/x-topcloud", ".tcl" : "application/x-tcl", ".tex" : "application/x-tex", ".texi" : "application/x-texinfo", ".texinfo" : "application/x-texinfo", ".text" : "text/plain", ".tif" : "image/tiff", ".tiff" : "image/tiff", ".torrent" : "application/x-bittorrent", ".tr" : "text/troff", ".ttf" : "application/x-font-ttf", ".txt" : "text/plain", ".vcf" : "text/x-vcard", ".vcs" : "text/x-vcalendar", ".vrml" : "model/vrml", ".war" : "application/java-archive", ".wav" : "audio/x-wav", ".wma" : "audio/x-ms-wma", ".wmv" : "video/x-ms-wmv", ".wmx" : "video/x-ms-wmx", ".wrl" : "model/vrml", ".wsdl" : "application/wsdl+xml", ".xbm" : "image/x-xbitmap", ".xhtml" : "application/xhtml+xml", ".xls" : "application/vnd.ms-excel", ".xml" : "application/xml", ".xpm" : "image/x-xpixmap", ".xsl" : "application/xml", ".xslt" : "application/xslt+xml", ".yaml" : "text/yaml", ".yml" : "text/yaml", ".zip" : "application/zip" } };