rest-handler
Version:
A module for creating a request handler that does REST style routing
398 lines (319 loc) • 11.3 kB
JavaScript
'use strict';
var querystring = require('querystring');
var AsyncValue = require('raptor-async/AsyncValue');
// intercepter for res.end
function end() {
// this function is always called in scope of response object
/* jshint validthis:true */
this._closed = true;
this._intercepted_end.apply(this, arguments);
}
function Accept(accept) {
var pos = accept.indexOf(';');
if (pos !== -1) {
this.params = accept.substring(pos+1);
accept = accept.substring(0, pos);
}
pos = accept.indexOf('/');
if (pos === -1) {
this.type = accept;
} else {
this.type = accept.substring(0, pos);
this.subtype = accept.substring(pos+1);
}
this.mimeType = (this.subtype ? this.type + '/' + this.subtype : this.type).toLowerCase();
}
Accept.prototype.getType = function() {
return this.type;
};
Accept.prototype.getSubtype = function() {
return this.subtype;
};
Accept.prototype.getMimeType = function() {
return this.mimeType;
};
function REST(handler, domain) {
this.handler = handler;
this.upgrade = false;
this.domain = domain;
}
function _jsonParser(str, callback) {
str = str.trim();
var body;
if (str.length > 0) {
try {
body = JSON.parse(str);
} catch(e) {
return callback('Invalid JSON: ' + e);
}
} else {
body = undefined;
}
callback(null, body);
}
var PARSERS = {
'application/x-www-form-urlencoded': function(str, callback) {
callback(null, querystring.parse(str));
},
'application/json': _jsonParser,
'text/plain': _jsonParser
};
REST.prototype = {
forwardTo: function(route) {
this.handler.forwardTo(this, route);
},
handle: function() {
var self = this;
// inherit the before functions from the RESTHandler
var allBefore = this.handler.getBefore();
var routeBefore = this.route._before;
var res = this.res;
if (res && !res._intercepted_end) {
res._intercepted_end = res.end;
res.end = end;
}
var i = -1;
// calculate the index where the general-purpose middleware ends
var allBeforeEnd = allBefore.length;
// calculate the index where the route-specific middleware ends
var routeBeforeEnd = 0;
if (routeBefore) {
routeBeforeEnd = allBefore.length + routeBefore.length;
}
var routeInvoked = false;
var next = function() {
var beforeHandler;
if (!self.isClosed()) {
i++;
if (i < allBeforeEnd) {
// We are in the range of overall middleware...
beforeHandler = allBefore[i];
// call the all middleware in the scope of the route
beforeHandler.call(self.route, self);
} else if (routeBeforeEnd && (i < routeBeforeEnd)) {
// We are in the range of route-specific middleware...
beforeHandler = routeBefore[i - allBefore.length];
// call the route-specific middleware in the scope of the route
beforeHandler.call(self.route, self);
} else if (routeInvoked === false) {
// set flag to indicate that we invoked the main route
routeInvoked = true;
// always delegate invocation to RESTHandler because this allows
// centralization of all route invocation
self.handler.invokeRoute(self);
} else {
// we already invoked the route but response is still not closed
// so interpret this as not found
self.notFound();
}
}
};
this._next = (this.domain) ? this.domain.bind(next) : next;
this._next();
},
isUpgrade: function() {
return this.upgrade === true;
},
next: function() {
if (this._next) {
this._next();
}
},
isClosed: function() {
return (this.upgrade !== true) && this.res._closed;
},
send: function(statusCode, obj) {
if (arguments.length === 1) {
obj = statusCode;
} else {
this.res.statusCode = statusCode;
}
this.handler.send(this, obj);
},
error: function(statusCode, error) {
if (arguments.length === 1) {
error = statusCode;
} else {
this.res.statusCode = statusCode;
}
this.handler.error(this, error);
},
getCookies: function() {
if (this._cookies === undefined) {
var req = this.req;
var cookies;
this._cookies = cookies = {};
if (req.headers.cookie) {
req.headers.cookie.split(';').forEach(function(cookie) {
var parts = cookie.split('=');
cookies[parts[0].trim()] = decodeURIComponent((parts[1] || '').trim());
});
}
}
return this._cookies;
},
getCookie: function(name) {
return this.getCookies()[name];
},
getBasicAuth: function() {
if (this._basicAuth === undefined) {
var req = this.req;
if (req.headers.authorization) {
var basicAuthParser = require('basic-auth-parser');
this._basicAuth = basicAuthParser(req.headers.authorization);
} else {
this._basicAuth = null;
}
}
return this._basicAuth;
},
getBodyBuffer: function(callback, limit) {
var self = this;
if (!self._bodyBuffer) {
self._bodyBuffer = new AsyncValue({
loader: function(callback) {
var len = 0;
var req = self.req;
var chunks = [];
var err;
req.on('data', function (data) {
len += data.length;
if (!limit || (len <= limit)) {
chunks.push(data);
} else {
err = new Error('Limit exceeded. Reached ' + len + ' characters. Limit = ' + limit);
// end the request
var res = self.res;
res.writeHead(413, {'Content-Type': 'text/plain'});
res.end('Limit of ' + limit +' bytes exceeded');
}
});
req.on('end', function () {
if (callback) {
if (err || (chunks.length === 0)) {
return callback(err);
}
callback(null, Buffer.concat(chunks, len));
}
});
}
});
}
self._bodyBuffer.done(callback);
},
getBody: function(callback, limit) {
var self = this;
if (!self._body) {
self._body = new AsyncValue({
loader: function(callback) {
self.getBodyBuffer(function(err, buffer) {
if (err) {
return callback(err);
}
if (!buffer) {
return callback();
}
return callback(null, buffer.toString('utf8'));
}, limit);
}
});
}
self._body.done(callback);
},
getParsedBody: function(callback, limit) {
var self = this;
if (!self._parsedBody) {
self._parsedBody = new AsyncValue({
loader: function(callback) {
self.getBody(function(err, body) {
if (err) {
return callback(err);
}
if (!body) {
return callback();
}
var contentType = self.req.headers['content-type'];
if (!contentType) {
contentType = 'application/json';
} else {
var pos = contentType.indexOf(';');
if (pos !== -1) {
contentType = contentType.substring(0, pos);
}
}
var parser = PARSERS[contentType];
if (!parser) {
return callback(new Error('Unrecognized content type for parsing: ' + contentType));
}
parser(body, callback);
}, limit);
}
});
}
self._parsedBody.done(callback);
if (limit && self._body) {
self._body.limit = Math.max(limit, self._body.limit);
}
},
setResponseHeader: function() {
this.res.setHeader.apply(this.res, arguments);
},
getResponseHeader: function() {
this.res.getHeader.apply(this.res, arguments);
},
getRequestHeaders: function() {
return this.req.headers;
},
getRequestHeader: function(name) {
return this.req.headers[name.toLowerCase()];
},
getAccepts: function() {
if (!this._accepts) {
var accepts = this.getRequestHeader('accept').split(',');
this._accepts = new Array(accepts.length);
this._acceptsMap = {};
for (var i = 0; i < accepts.length; i++) {
var accept = new Accept(accepts[i]);
this._accepts[i] = accept;
this._acceptsMap[accept.getMimeType()] = accept;
}
}
return this._accepts;
},
getPreferredContentType: function(candidates) {
var accepts = this.getAccepts();
if (accepts.length === 0) {
return null;
}
if (candidates) {
var supported = {};
var i;
if (Array.isArray(candidates)) {
for (i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
supported[candidate.toLowerCase()] = candidate;
}
} else {
supported[candidates.toLowerCase()] = candidates;
}
for (i = 0; i < accepts.length; i++) {
var accept = accepts[i];
var match = supported[accept.getMimeType()];
if (match) {
return match;
}
}
return null;
} else {
return accepts[0];
}
},
notFound: function(message) {
if (arguments.length === 0) {
this.handler.notFound(this.req, this.res);
} else {
this.handler.notFound(message, this.req, this.res);
}
}
};
module.exports = REST;