UNPKG

rapid-rest

Version:

Minimal rest server

248 lines (224 loc) 10.2 kB
/** * Created with JetBrains WebStorm. * User: jrudland * Date: 26/06/12 * Time: 11:02 PM * To change this template use File | Settings | File Templates. */ var http = require('http'), url = require('url'), domain = require('domain'), cluster = require('cluster'); module.exports = function (options) { /* Syntax to enable the following invocation signature : routes('/my_path')('get', myFunc); */ var self = this, opts = options || {}, rest = function (route, isCaseInsensitive) { return rest.handlers(rest.router.add(route, isCaseInsensitive)); }; self.allocateCPUs = opts.allocateCPUs || 2; rest.matchDefs = {}; rest.methods = {}; rest.respond = function (res, code, headers, data) { // Handles empty responses more gracefully for feedback purposes if (code && headers && !data) { data = headers; headers = {}; } else if (code && !headers && !data) { data = code; code = 200; headers = {}; } // Ready the package if (typeof data === 'object') { data = JSON.stringify(data); headers['Content-type'] = headers['Content-type'] || 'application/json'; } // Playing nice header that's not really required if (!headers['Content-length'] && typeof data === 'string') { headers['Content-length'] = data.length; } res.writeHead(code, headers); res.end(data); }; rest.handlers = function (route){ // Create options collection if it doesn't exist route.methods.options = route.methods.options || function (req, res, params) { var methods = Object.keys(route.methods).filter(function (key) { return key !== 'options'; }); rest.respond(res, 200, methods); }; // Request services are found at the intersection of service pattern and verb name route.method = function (rawVerb, accept, fn) { // Handle overloads if (rawVerb && accept && !fn) { fn = accept; accept = null; } var verb = rawVerb.toLowerCase(); if (verb[0] !== 'p') { // verbs that don't send data such as GET, SEARCH route.methods[verb] = function (req, res, segments) { req.on('error', function (err) { // Client did something bizarre; // req.connection.destroy(); }); fn(req, res, segments); }; } else { // Expecting POST or PUT verb route.methods[verb] = function (req, res, segments) { var body = ''; // Data accepting event req.on('error', function (err) { // Client did something bizarre; // req.connection.destroy(); }); req.on('data', function (data) { body += data; if (body.length > 1e6) { // FLOOD ATTACK OR FAULTY CLIENT, NUKE REQUEST req.connection.destroy(); } }); req.on('end', function () { var data, files; // For performance reasons will only ever support JSON if (/application\/json/i.test(req.headers["content-type"])) { try { // Invoke the callback data = (!body.length) ? null : JSON.parse(body); fn(req, res, segments, data); } catch(ejson) { res.write("ERROR: Could not parse body as json.\n\n" + body); res.end(); } } else { res.write("ERROR: Content-type not supported:" + req.headers["content-type"]); res.end(); } }); }; } return route.method; }; return route.method; }; // Routes to be handled rest.router = (function () { var routes = {}; return { add : function (urlPath, isCaseInsensitive) { //Performance gains are made here by setting up all variables required for the runtime service var urlDef = url.parse(urlPath, true), urlMatch = urlDef.pathname.replace(/[\-\[\]\{\}\(\)\*\+\?\.,\/\\\^\$\|#\s]/g, "\\$&").replace(/(:[^(\\/)]+)/g, "([^/\\?]*)"), paramKeys = (urlPath.indexOf("/:") > -1) ? urlDef.pathname.replace(/(\/[^:][^\/]+)/g, '').substr(2).split("/:") : [], queryKeys = urlDef.query; //QueryStrings are optional and not included in the match if (urlPath.indexOf("?") === -1) { urlMatch += "(?=\\?.*|$)"; } //Remove escaped reserved '^' character if (urlPath[0] === '^') { urlMatch = urlMatch.substr(1); } rest.matchDefs[urlMatch] = rest.matchDefs[urlMatch] || {re: new RegExp(urlMatch, isCaseInsensitive ? "i" : ""), paramKeys: paramKeys, queryKeys: queryKeys, methods: {}}; return rest.matchDefs[urlMatch]; }, handle : function (req, res) { var paramKeys, urlDef, pattern, key, ikey, matchDef, urlParams, queryMatch, method = req.method.toLowerCase(), location = routes, urlPath = req.url, params = {}; //Optimization for URLs that have no query string if (req.url.indexOf("?") >= 0) { urlDef = url.parse(req.url, true); urlPath = urlDef.pathname; req.query = urlDef.query; } // Search for matching pattern for (pattern in rest.matchDefs) { matchDef = rest.matchDefs[pattern]; urlParams = urlPath.match(matchDef.re); // Successful match if (urlParams !== null) { // If required, validate querystring contains the required value pairs queryMatch = true; if (urlDef && matchDef.queryKeys) { for (key in matchDef.queryKeys) { if (!urlDef.query[key]) { queryMatch = false; } // Copy query string key pairs to params collection params[key] = req.query[key]; } } // Align parameters in the URL with the names from the service definition paramKeys = rest.matchDefs[pattern].paramKeys; for (ikey = 0; ikey < paramKeys.length; ikey++) { params[paramKeys[ikey]] = urlParams[ikey + 1]; } // Assign the route if the entire pattern matches if (queryMatch) { location = rest.matchDefs[pattern]; } break; } } // The service has not been defined if (!location.methods || !location.methods[method]) { return rest.respond(res, 501, {'Content-type' : 'text/plain'}, 'Not Implemented'); } location.methods[method](req, res, params); } }; }()); rest.listen = function (host, port, cb) { var server, icpu; if (cluster.isMaster && self.allocateCPUs > 1) { for (icpu = 0; icpu < self.allocateCPUs; icpu++) { cluster.fork(); } cluster.on('disconnect', function (worker) { console.error('disconnect!'); cluster.fork(); }); } else { server = http.createServer(function (req, res) { var d = domain.create(); d.on('error', function (er) { console.error('error', er.stack); try { var killtimer = setTimeout(function () { process.exit(1); }, 10000); // process.unref(); // stop taking new requests. server.close(); // When allocating more than 1 CPU if (cluster.worker) { cluster.worker.disconnect(); } if (!res.headersSent) { res.statusCode = 500; res.setHeader('content-type', 'text/plain'); res.end('Oops, there was a problem!\n'); } } catch (er2) { // oh well, not much we can do at this point. console.error('Error sending 500!', er2.stack); } }); d.add(req); d.add(res); d.run(function () { rest.router.handle(req, res); }); }); server.listen(host, port, cb); } }; return rest; };