Comparing version 0.5.6 to 1.0.0-1-rc
1245
lib/server.js
@@ -1,1030 +0,475 @@ | ||
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
// Copyright 2011 Mark Cavage, Inc. All rights reserved. | ||
var assert = require('assert'); | ||
var crypto = require('crypto'); | ||
var EventEmitter = require('events').EventEmitter; | ||
var http = require('http'); | ||
var https = require('https'); | ||
var path = require('path'); | ||
var querystring = require('querystring'); | ||
var url = require('url'); | ||
var util = require('util'); | ||
var formidable = require('formidable'); | ||
var semver = require('semver'); | ||
var uuid = require('node-uuid'); | ||
var xml2js = require('xml2js'); | ||
var errors = require('./errors'); | ||
var Request = require('./request'); | ||
var Response = require('./response'); | ||
var Route = require('./route'); | ||
var HttpCodes = require('./http_codes'); | ||
var RestCodes = require('./rest_codes'); | ||
var log = require('./log'); | ||
var errors = require('./error'); | ||
var sprintf = require('./sprintf').sprintf; | ||
// Just force this to extend http.ServerResponse | ||
require('./http_extra'); | ||
var newError = errors.newError; | ||
///--- Globals | ||
var BadMethodError = errors.BadMethodError; | ||
var InvalidVersionError = errors.InvalidVersionError; | ||
var ResourceNotFoundError = errors.ResourceNotFoundError; | ||
///--- Internal Helpers | ||
/** | ||
* Cleans up sloppy URL paths, like /foo////bar/// to /foo/bar. | ||
* | ||
* @param {String} path the HTTP resource path. | ||
* @return {String} Cleaned up form of path. | ||
*/ | ||
function _sanitizePath(path) { | ||
assert.ok(path); | ||
///--- Helpers | ||
if (log.trace()) | ||
log.trace('_sanitizePath: path=%s', path); | ||
function argsToChain() { | ||
assert.ok(arguments.length); | ||
// Be nice like apache and strip out any //my//foo//bar///blah | ||
var _path = path.replace(/\/\/+/g, '/'); | ||
var args = arguments[0]; | ||
if (args.length < 0) | ||
throw new TypeError('handler (Function) required'); | ||
// Kill a trailing '/' | ||
if (_path.lastIndexOf('/') === (_path.length - 1) && | ||
_path.length > 1) { | ||
_path = _path.substr(0, _path.length - 1); | ||
} | ||
var chain = []; | ||
if (log.trace()) | ||
log.trace('_sanitizePath: returning %s', _path); | ||
function process(handlers) { | ||
handlers.forEach(function(h) { | ||
if (Array.isArray(h)) | ||
return process(h); | ||
if (!typeof(h) === 'function') | ||
throw new TypeError('handlers must be Functions'); | ||
return decodeURIComponent(_path); | ||
} | ||
/** | ||
* Checks if a mount matches, and if so, returns an object with all | ||
* the :param variables. | ||
* | ||
* @param {String} path (request.url.pathname). | ||
* @param {Object} route (what was mounted). | ||
*/ | ||
function _matches(path, route) { | ||
assert.ok(path); | ||
assert.ok(route); | ||
if (route.regex) | ||
return route.url.exec(path); | ||
if (path === route.url) | ||
return {}; // there were no params if it was an exact match | ||
var params = route.urlComponents; | ||
var components = path.split('/').splice(1); | ||
var len = components.length; | ||
if (components.length !== params.length) | ||
return null; | ||
var parsed = {}; | ||
for (var i = 0; i < params.length; i++) { | ||
// Don't use URL.parse, as it doesn't handle strings | ||
// with ':' in them for this case. Regardless of what the | ||
// RFC says about this, people do it. | ||
var frag = components[i]; | ||
if (frag.indexOf('?') !== -1) | ||
frag = frag.split('?', 2)[0]; | ||
if (params[i] === frag) | ||
continue; | ||
if (params[i].charAt(0) === ':') { | ||
if (/.+\.(xml|json)$/.test(frag)) | ||
frag = frag.split(/\.(xml|json)$/)[0]; | ||
parsed[params[i].substr(1)] = frag; | ||
continue; | ||
} | ||
return null; | ||
return chain.push(h); | ||
}); | ||
} | ||
process(Array.prototype.slice.call(args, 0)); | ||
return parsed; | ||
return chain; | ||
} | ||
function _parseAccept(request, response) { | ||
assert.ok(request); | ||
assert.ok(response); | ||
assert.ok(request._config.acceptable); | ||
assert.ok(request._config.acceptable.length); | ||
function logRequest(req) { | ||
assert.ok(req); | ||
if (!request.headers.accept) { | ||
log.trace('_parseAccept: no accept header sent, using `default`'); | ||
response._accept = request.headers.accept = request._config.acceptable[0]; | ||
return true; | ||
} | ||
if (req.log.isTraceEnabled()) | ||
req.log.trace('New Request:\n\n%s', req.toString()); | ||
} | ||
var mediaRanges = request.headers.accept.split(','); | ||
for (var i = 0; i < mediaRanges.length; i++) { | ||
var _accept = new RegExp(mediaRanges[i].split(';')[0].replace(/\*/g, '.*')); | ||
for (var j = 0; j < request._config.acceptable.length; j++) { | ||
if (_accept.test(request._config.acceptable[j])) { | ||
response._accept = request._config.acceptable[j]; | ||
log.trace('Parsed accept type as: %s', response._accept); | ||
return true; | ||
} | ||
} | ||
} | ||
var msg = JSON.stringify({ | ||
code: 'InvalidHeader', | ||
message: '\'Accept: ' + request.headers.accept + '\' not supported.', | ||
supported: request._config.acceptable | ||
}, null, 2); | ||
response.send(HttpCodes.NotAcceptable, msg, {'content-type': 'text/plain'}); | ||
return false; | ||
function default404Handler(req, res) { | ||
res.send(new ResourceNotFoundError(req.url + ' not found')); | ||
} | ||
function _parseAuthorization(req, res) { | ||
req.authorization = {}; | ||
req.username = 'anonymous'; | ||
if (!req.headers.authorization) { | ||
log.trace('No authorization header present.'); | ||
return true; | ||
} | ||
var pieces = req.headers.authorization.split(' ', 2); | ||
if (!pieces || pieces.length !== 2) { | ||
res.sendError(newError({ | ||
httpCode: HttpCodes.BadRequest, | ||
restCode: RestCodes.InvalidHeader, | ||
message: 'BasicAuth content is invalid.' | ||
})); | ||
return false; | ||
} | ||
req.authorization = { | ||
scheme: pieces[0], | ||
credentials: pieces[1] | ||
}; | ||
if (pieces[0] === 'Basic') { | ||
var decoded = (new Buffer(pieces[1], 'base64')).toString('utf8'); | ||
if (!decoded) { | ||
res.sendError(newError({ | ||
httpCode: HttpCodes.BadRequest, | ||
restCode: RestCodes.InvalidHeader, | ||
message: 'Authorization: Basic content is invalid (not base64).' | ||
})); | ||
return false; | ||
} | ||
if (decoded !== null) { | ||
var idx = decoded.indexOf(':'); | ||
if (idx === -1) { | ||
pieces = [decoded]; | ||
} else { | ||
pieces = [decoded.slice(0, idx), decoded.slice(idx + 1)]; | ||
} | ||
} | ||
if (!(pieces !== null ? pieces[0] : null) || | ||
!(pieces !== null ? pieces[1] : null)) { | ||
res.sendError(newError({ | ||
httpCode: HttpCodes.BadRequest, | ||
restCode: RestCodes.InvalidHeader, | ||
message: 'Authorization: Basic content is invalid.' | ||
})); | ||
return false; | ||
} | ||
req.authorization.basic = { | ||
username: pieces[0], | ||
password: pieces[1] | ||
}; | ||
req.username = pieces[0]; | ||
function default405Handler(req, res, methods) { | ||
res.header('Allow', methods.join(', ')); | ||
if (req.method === 'OPTIONS') { | ||
res.send(200); | ||
} else { | ||
log.debug('Unknown authorization scheme %s. Skipping processing', | ||
req.authorization.scheme); | ||
var msg = req.url + ' does not support ' + req.method; | ||
res.send(new BadMethodError(msg)); | ||
} | ||
return true; | ||
} | ||
function _parseDate(request, response) { | ||
if (request.headers.date) { | ||
try { | ||
var date = new Date(request.headers.date); | ||
var now = new Date(); | ||
function defaultBadVersionHandler(req, res, versions) { | ||
var msg = req.method + ' ' + req.url + ' supports versions: ' + | ||
versions.join(', '); | ||
if (log.trace()) | ||
log.trace('Date: sent=%d, now=%d, allowed=%d', | ||
date.getTime(), now.getTime(), request._config.clockSkew); | ||
if ((now.getTime() - date.getTime()) > request._config.clockSkew) { | ||
response.sendError(newError({ | ||
httpCode: HttpCodes.BadRequest, | ||
restCode: RestCodes.InvalidArgument, | ||
message: 'Date header is too old' | ||
})); | ||
return false; | ||
} | ||
} catch (e) { | ||
if (log.trace()) | ||
log.trace('Bad Date header: ' + e); | ||
response.sendError(newError({ | ||
httpCode: HttpCodes.BadRequest, | ||
restCode: RestCodes.InvalidArgument, | ||
message: 'Date header is invalid' | ||
})); | ||
return false; | ||
} | ||
} | ||
return true; | ||
res.send(new InvalidVersionError(msg)); | ||
} | ||
function _parseApiVersion(request, response) { | ||
if (request.headers['x-api-version']) | ||
request._version = request.headers['x-api-version']; | ||
return true; | ||
function toPort(x) { | ||
x = parseInt(x, 10); | ||
return (x >= 0 ? x : false); | ||
} | ||
function _parseQueryString(request, response) { | ||
request._url = url.parse(request.url); | ||
if (request._url.query) { | ||
var _qs = querystring.parse(request._url.query); | ||
for (var k in _qs) { | ||
if (_qs.hasOwnProperty(k)) { | ||
assert.ok(!request.params[k]); | ||
request.params[k] = _qs[k]; | ||
} | ||
} | ||
} | ||
return true; | ||
function isPipeName(s) { | ||
return (typeof(s) === 'string' && toPort(s) === false); | ||
} | ||
function _parseHead(request, response) { | ||
assert.ok(request); | ||
assert.ok(response); | ||
log.trace('_parseHead:\n%s %s HTTP/%s\nHeaders: %o', | ||
request.method, | ||
request.url, | ||
request.httpVersion, | ||
request.headers); | ||
///--- API | ||
if (!_parseAccept(request, response)) return false; | ||
if (!_parseAuthorization(request, response)) return false; | ||
if (!_parseDate(request, response)) return false; | ||
if (!_parseApiVersion(request, response)) return false; | ||
if (!_parseQueryString(request, response)) return false; | ||
/** | ||
* Constructor. Creates a REST API Server. | ||
* | ||
* - options {Object} construction arguments. (log4js required). | ||
*/ | ||
function Server(options) { | ||
if (typeof(options) !== 'object') | ||
throw new TypeError('options (Object) required'); | ||
if (typeof(options.dtrace) !== 'object') | ||
throw new TypeError('options.dtrace (Object) required'); | ||
if (typeof(options.log4js) !== 'object') | ||
throw new TypeError('options.log4js (Object) required'); | ||
return true; | ||
} | ||
EventEmitter.call(this); | ||
this.chain = []; | ||
this.formatters = options.formatters || {}; | ||
this.log4js = options.log4js; | ||
this.log = this.log4js.getLogger('Server'); | ||
this.name = options.name || 'restify'; | ||
this.routes = []; | ||
this.version = options.version || false; | ||
function _parseRequest(request, response, next) { | ||
assert.ok(request); | ||
assert.ok(response); | ||
assert.ok(next); | ||
var contentType = request.contentType(); | ||
if (contentType === 'multipart/form-data') { | ||
var form = formidable.IncomingForm(); | ||
form.maxFieldsSize = request._config.maxRequestSize; | ||
form.on('error', function(err) { | ||
return response.sendError(newError({ | ||
httpCode: HttpCodes.BadRequest, | ||
restCode: RestCodes.BadRequest, | ||
message: err.toString() | ||
})); | ||
var secure = false; | ||
if (options.certificate && options.key) { | ||
secure = true; | ||
this.server = https.createServer({ | ||
cert: options.certificate, | ||
key: options.key | ||
}); | ||
} else { | ||
this.server = http.createServer(); | ||
} | ||
form.on('field', function(field, value) { | ||
log.trace('_parseRequest(multipart) field=%s, value=%s', field, value); | ||
request.params[field] = value; | ||
}); | ||
var self = this; | ||
this.server.on('error', function(err) { | ||
self.emit('error', err); | ||
}); | ||
form.on('end', function() { | ||
log.trace('_parseRequset(multipart): req.params=%o', request.params); | ||
return next(); | ||
}); | ||
form.parse(request); | ||
this.server.on('clientError', function(err) { | ||
self.emit('clientError', err); | ||
}); | ||
} else { | ||
request.body = ''; | ||
request.on('data', function(chunk) { | ||
request.body += chunk; | ||
if (request.body.length > request._config.maxRequestSize) { | ||
return response.sendError(newError({ | ||
httpCode: HttpCodes.RequestTooLarge, | ||
restCode: RestCodes.RequestTooLarge, | ||
message: 'maximum HTTP data size is 8k' | ||
})); | ||
} | ||
}); | ||
this.server.on('close', function() { | ||
self.emit('close'); | ||
}); | ||
request.on('end', function() { | ||
function done(err, bParams) { | ||
if (err) { | ||
return response.sendError(newError({ | ||
httpCode: HttpCodes.BadRequest, | ||
restCode: RestCodes.InvalidArgument, | ||
message: 'Invalid Content: ' + err.message | ||
})); | ||
} | ||
this.server.on('connection', function(socket) { | ||
self.emit('connection', socket); | ||
}); | ||
Object.keys(bParams).forEach(function(k) { | ||
if (request.params.hasOwnProperty(k)) { | ||
return response.sendError(newError({ | ||
httpCode: HttpCodes.Conflict, | ||
restCode: RestCodes.InvalidArgument, | ||
message: 'duplicate parameter detected: ' + k | ||
})); | ||
} | ||
request.params[k] = bParams[k]; | ||
}); | ||
this.server.on('upgrade', function(request, socket, headPacket) { | ||
return self.emit('upgrade', request, socket, headPacket); | ||
}); | ||
log.trace('_parseRequest: params parsed as: %o', request.params); | ||
return next(); | ||
} | ||
this.server.on('request', function(req, res) { | ||
return self._request(req, res); | ||
}); | ||
if (request.body) { | ||
log.trace('_parseRequest: req.body=%s', request.body); | ||
this.server.on('checkContinue', function(req, res) { | ||
return self._request(req, res, true); | ||
}); | ||
var contentLen = request.headers['content-length']; | ||
if (contentLen !== undefined) { | ||
var actualLen = Buffer.byteLength(request.body, 'utf8'); | ||
if (parseInt(contentLen, 10) !== actualLen) { | ||
return response.sendError(newError({ | ||
httpCode: HttpCodes.BadRequest, | ||
restCode: RestCodes.InvalidHeader, | ||
message: 'Content-Length=' + contentLen + | ||
' didn\'t match actual length=' + actualLen | ||
})); | ||
} | ||
} | ||
var bParams; | ||
if (request._config.contentHandlers[contentType]) { | ||
try { | ||
bParams = request._config.contentHandlers[contentType](request.body, | ||
request, | ||
response, | ||
done); | ||
if (bParams) | ||
return done(null, bParams); | ||
} catch (e) { | ||
return done(e); | ||
} | ||
} else if (contentType) { | ||
return response.sendError(newError({ | ||
httpCode: HttpCodes.UnsupportedMediaType, | ||
restCode: RestCodes.InvalidArgument, | ||
message: contentType + ' unsupported' | ||
})); | ||
} | ||
} else { | ||
return done(null, {}); | ||
} | ||
this.__defineGetter__('acceptable', function() { | ||
var accept = Object.keys(self.formatters) || []; | ||
Response.ACCEPTABLE.forEach(function(a) { | ||
if (accept.indexOf(a) === -1) | ||
accept.push(a); | ||
}); | ||
} | ||
} | ||
return accept; | ||
}); | ||
function _handleNoRoute(server, request, response) { | ||
assert.ok(server); | ||
assert.ok(request); | ||
assert.ok(response); | ||
this.__defineGetter__('name', function() { | ||
return options.name || 'restify'; | ||
}); | ||
var body = null; | ||
var code = HttpCodes.NotFound; | ||
var headers = {}; | ||
this.__defineGetter__('dtrace', function() { | ||
return options.dtrace; | ||
}); | ||
// This is such a one-off that it's saner to just handle it as such | ||
if (request.method === 'OPTIONS' && | ||
request.url === '*') { | ||
code = HttpCodes.Ok; | ||
} else { | ||
var urls = server.routes.urls; | ||
for (var u in urls) { | ||
if (urls.hasOwnProperty(u)) { | ||
var route = urls[u]; | ||
var methods = []; | ||
var versions = []; | ||
this.__defineGetter__('url', function() { | ||
if (self.socketPath) | ||
return 'http://' + self.socketPath; | ||
var matched = false; | ||
for (var i = 0; i < route.length; i++) { | ||
if (methods.indexOf(route[i].method) === -1) | ||
methods.push(route[i].method); | ||
if (route[i].version && versions.indexOf(route[i].version) === -1) | ||
versions.push(route[i].version); | ||
if (_matches(request.url, route[i])) | ||
matched = true; | ||
} | ||
if (matched) { | ||
code = HttpCodes.BadMethod; | ||
response._allowedMethods = methods; | ||
if (versions.length) { | ||
headers['x-api-versions'] = versions.join(', '); | ||
if (methods.indexOf(request.method) !== -1) { | ||
code = HttpCodes.RetryWith; | ||
body = { | ||
code: 'InvalidVersion', | ||
message: 'Retry with an x-api-version header' | ||
}; | ||
} | ||
} | ||
if (request.method === 'OPTIONS') { | ||
code = HttpCodes.Ok; | ||
headers.Allow = methods.join(', '); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
response.send(code, body, headers); | ||
return log.w3cLog(request, response, function() {}); | ||
var str = secure ? 'https://' : 'http://'; | ||
str += self.address().address; | ||
str += ':'; | ||
str += self.address().port; | ||
return str; | ||
}); | ||
} | ||
util.inherits(Server, EventEmitter); | ||
module.exports = Server; | ||
// --- Publicly available API | ||
Server.prototype.address = function address() { | ||
return this.server.address(); | ||
}; | ||
module.exports = { | ||
/** | ||
* Gets the server up and listening. | ||
* | ||
* You can call like: | ||
* server.listen(80) | ||
* server.listen(80, '127.0.0.1') | ||
* server.listen('/tmp/server.sock') | ||
* | ||
* And pass in a callback to any of those forms. Also, by default, invoking | ||
* this method will trigger DTrace probes to be enabled; to not do that, pass | ||
* in 'false' as the second to last parameter. | ||
* | ||
* @param {Function} callback optionally get notified when listening. | ||
* @throws {TypeError} on bad input. | ||
*/ | ||
Server.prototype.listen = function listen() { | ||
var callback = false; | ||
var dtrace = true; | ||
var self = this; | ||
/** | ||
* Creates a new restify HTTP server. | ||
* | ||
* @param {Object} options a hash of configuration parameters: | ||
* - serverName: String to send back in the Server header. | ||
* Default: node.js. | ||
* - maxRequestSize: Max request size to allow, in bytes. | ||
* Default: 8192. | ||
* - accept: Array of valid MIME types to allow in Accept. | ||
* Default: application/json. | ||
* - version: Default API version to support; setting this | ||
* means clients are required to send an | ||
* X-Api-Version header. | ||
* Default: None. | ||
* - clockSkew: If a Date header is present, allow N seconds | ||
* of skew. | ||
* Default: 300 | ||
* - logTo: a `Writable Stream` where log messages should go. | ||
* Default: process.stderr. | ||
* - contentHandlers: An object of | ||
* 'type'-> function(body, req, res). | ||
* Built in content types are: | ||
* - application/json | ||
* - application/x-www-form-urlencoded | ||
* - contentWriters: An object of | ||
* 'type' -> function(obj, req, res). | ||
* - Additionally supports application/javascript (jsonp) | ||
* - headers: An object of global headers that are sent back | ||
* on all requests. Restify automatically sets: | ||
* - X-Api-Version (if versioned) | ||
* - X-Request-Id | ||
* - X-Response-Time | ||
* - Content-(Length|Type|MD5) | ||
* - Access-Control-Allow-(Origin|Methods|Headers) | ||
* - Access-Control-Expose-Headers | ||
* If you don't set those particular keys, restify | ||
* fills in default functions; if you do set them, | ||
* you can fully override restify's defaults. | ||
* Note that like contentWriters, this is an object | ||
* with a string key as the header name, and the | ||
* value is a function of the form f(response) | ||
* which must return a string. | ||
* | ||
* @return {Object} node HTTP server, with restify "magic". | ||
*/ | ||
createServer: function(options) { | ||
var k; | ||
var server; | ||
function listenCallback() { | ||
if (dtrace) | ||
self.dtrace.enable(); | ||
function httpMain(request, response) { | ||
assert.ok(request); | ||
assert.ok(response); | ||
return callback ? callback.call(self) : false; | ||
} | ||
var route; | ||
var path = _sanitizePath(request.url); | ||
request.url = path; | ||
request.username = ''; | ||
if (!arguments.length) | ||
return this.server.listen(listenCallback); | ||
request.requestId = response.requestId = uuid().toLowerCase(); | ||
request.startTime = response.startTime = new Date().getTime(); | ||
callback = arguments[arguments.length - 1]; | ||
if (typeof(callback) !== 'function') | ||
callback = false; | ||
request._config = server._config; | ||
request.params = {}; | ||
request.uriParams = {}; | ||
if (arguments.length >= 2 && arguments[arguments.length - 2] === false) | ||
dtrace = false; | ||
response.request = request; | ||
response._method = request.method; | ||
response._allowedMethods = ['OPTIONS']; | ||
response._config = server._config; | ||
response._sent = false; | ||
response._errorSent = false; | ||
response._formatError = server._config.formatError; | ||
switch (typeof(arguments[0])) { | ||
case 'function': | ||
return this.server.listen(listenCallback); | ||
// HTTP and HTTPS are different -> joyent/node GH #1005 | ||
var addr = request.connection.remoteAddress; | ||
if (!addr) { | ||
if (request.connection.socket) { | ||
addr = request.connection.socket.remoteAddress; | ||
} else { | ||
addr = 'unknown'; | ||
} | ||
} | ||
request._remoteAddress = addr; | ||
response._remoteAddress = addr; | ||
case 'string': | ||
if (isPipeName(arguments[0])) | ||
return this.server.listen(arguments[0], listenCallback); | ||
if (!_parseHead(request, response)) | ||
return log.w3cLog(request, response, function() {}); | ||
throw new TypeError(arguments[0] + ' is not a named pipe'); | ||
if (!server.routes[request.method]) | ||
return _handleNoRoute(server, request, response); | ||
case 'number': | ||
var host = arguments[1]; | ||
return this.server.listen(arguments[0], | ||
typeof(host) === 'string' ? host : '0.0.0.0', | ||
listenCallback); | ||
var routes = server.routes[request.method]; | ||
for (var i = 0; i < routes.length; i++) { | ||
default: | ||
throw new TypeError('port (Number) required'); | ||
} | ||
}; | ||
var params = _matches(path, routes[i]); | ||
if (params) { | ||
// If the server isn't using versioning, just ignore | ||
// whatever the client sent as a version header. | ||
// If the server is, the client MUST send a version | ||
// header. Unless the server is configured | ||
// with weak versioning. | ||
var ok = true; | ||
if (routes[i].version && !server.weakVersions) { | ||
if (request._version) { | ||
if (routes[i].semver) { | ||
ok = semver.satisfies(routes[i].version, request._version); | ||
} else { | ||
ok = (routes[i].version === request._version); | ||
} | ||
} else { | ||
ok = false; | ||
} | ||
} | ||
if (ok) { | ||
request.uriParams = params; | ||
route = routes[i]; | ||
if (route.version) | ||
response._version = route.version; | ||
/** | ||
* Shuts down this server, and invokes callback (optionally) when done. | ||
* | ||
* @param {Function} callback optional callback to invoke when done. | ||
*/ | ||
Server.prototype.close = function close(callback) { | ||
if (callback) { | ||
if (typeof(callback) !== 'function') | ||
throw new TypeError('callback must be a function'); | ||
for (var j = 0; j < server.routes.urls[route.url].length; j++) { | ||
var r = server.routes.urls[route.url][j]; | ||
response._allowedMethods.push(r.method); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
this.server.once('close', function() { | ||
return callback(); | ||
}); | ||
} | ||
if (!route) | ||
return _handleNoRoute(server, request, response); | ||
return this.server.close(); | ||
}; | ||
log.trace('%s %s route found -> %o', request.method, request.url, route); | ||
var handler = 0; | ||
var chain; | ||
var stage = 0; | ||
// Register all the routing methods | ||
['del', 'get', 'head', 'post', 'put'].forEach(function(method) { | ||
function runChain() { | ||
return _parseRequest(request, response, function(had_err) { | ||
var next = arguments.callee; | ||
/** | ||
* Mounts a chain on the given path against this HTTP verb | ||
* | ||
* @param {Object} options the URL to handle, at minimum. | ||
* @return {Route} the newly created route. | ||
*/ | ||
Server.prototype[method] = function(options) { | ||
if (arguments.length < 2) | ||
throw new Error('At least one handler (Function) required'); | ||
if (had_err) | ||
response.sendError(had_err); | ||
if (typeof(options) !== 'object' && typeof(options) !== 'string') | ||
throw new TypeError('path (String) required'); | ||
if (chain.length > 1) { | ||
// Check if we need to skip to the post chain | ||
if ((stage === 0 && response._sent) || | ||
(stage === 1 && response._errorSent)) { | ||
stage = 2; | ||
handler = 0; | ||
} | ||
} | ||
var args = Array.prototype.slice.call(arguments, 1); | ||
if (!chain[stage]) | ||
return; | ||
if (method === 'del') | ||
method = 'DELETE'; | ||
if (!chain[stage][handler]) { | ||
if (++stage >= chain.length) | ||
return; | ||
return this._addRoute(method.toUpperCase(), options, args); | ||
}; | ||
}); | ||
handler = 0; | ||
} | ||
if (chain[stage][handler]) | ||
return chain[stage][handler++].call(this, request, response, next); | ||
}); | ||
} | ||
/** | ||
* Removes a route from the server. | ||
* | ||
* You can either pass in the route name or the route object as `name`. | ||
* | ||
* @param {String} name the route name. | ||
* @return {Boolean} true if route was removed, false if not. | ||
* @throws {TypeError} on bad input. | ||
*/ | ||
Server.prototype.rm = function rm(name) { | ||
if (typeof(name) !== 'string' && !(name instanceof Route)) | ||
throw new TypeError('name (String) required'); | ||
if (route.handlers.pre && | ||
route.handlers.main && | ||
route.handlers.post) { | ||
chain = [ | ||
route.handlers.pre, | ||
route.handlers.main, | ||
route.handlers.post | ||
]; | ||
} else { | ||
chain = [route.handlers.main]; | ||
} | ||
return runChain(); | ||
} // end httpMain | ||
if (options && options.cert && options.key) { | ||
server = https.createServer(options, httpMain); | ||
} else { | ||
server = http.createServer(httpMain); | ||
for (var i = 0; i < this.routes.length; i++) { | ||
if (this.routes[i].name === name || this.routes[i] === name) { | ||
this.routes.splice(i, 1); | ||
return true; | ||
} | ||
} | ||
server.logLevel = function(level) { | ||
return log.level(level); | ||
}; | ||
return false; | ||
}; | ||
server.routes = {}; | ||
server._config = {}; | ||
server._config.contentHandlers = {}; | ||
server._config.contentWriters = {}; | ||
server._config.headers = {}; | ||
if (!options) | ||
options = {}; | ||
/** | ||
* Installs a list of handlers to run _before_ the "normal" handlers of all | ||
* routes. | ||
* | ||
* You can pass in any combination of functions or array of functions. | ||
* | ||
* @throws {TypeError} on input error. | ||
*/ | ||
Server.prototype.use = function use() { | ||
var chain = argsToChain(arguments); | ||
if (options.serverName) | ||
server._config.serverName = options.serverName; | ||
if (chain.length) { | ||
var self = this; | ||
chain.forEach(function(h) { | ||
self.chain.push(h); | ||
}); | ||
if (options.apiVersion && !options.version) | ||
options.version = options.apiVersion; | ||
this.routes.forEach(function(r) { | ||
r.use(chain); | ||
}); | ||
} | ||
if (options.version) | ||
server._config.version = options.version; | ||
return this; | ||
}; | ||
if (options.maxRequestSize) | ||
server._config.maxRequestSize = options.maxRequestSize; | ||
if (options.acceptable) | ||
server._config.acceptable = options.acceptable; | ||
if (options.accept) | ||
server._config.acceptable = options.accept; | ||
///--- Private methods | ||
if (options.clockSkew) | ||
server._config.clockSkew = options.clockSkew * 1000; | ||
Server.prototype._addRoute = function _addRoute(method, options, handlers) { | ||
var self = this; | ||
if (options.logTo) | ||
log.stderr(options.logTo); | ||
var chain = this.chain.slice(0); | ||
argsToChain(handlers).forEach(function(h) { | ||
chain.push(h); | ||
}); | ||
if (options.fullErrors) | ||
server._config.fullErrors = true; | ||
if (typeof(options) !== 'object') | ||
options = { url: options }; | ||
if (options.sendErrorLogger) | ||
server._config.sendErrorLogger = options.sendErrorLogger; | ||
var route = new Route({ | ||
log4js: self.log4js, | ||
method: method, | ||
url: options.path || options.url, | ||
handlers: chain, | ||
name: options.name, | ||
version: options.version || self.version, | ||
dtrace: self.dtrace | ||
}); | ||
route.on('error', function(err) { | ||
self.emit('error', err); | ||
}); | ||
route.on('done', function(req, res) { | ||
self.emit('after', req, res, route.name); | ||
}); | ||
// begin contentHandlers/writers | ||
if (!options.contentHandlers) | ||
options.contentHandlers = {}; | ||
if (!options.contentWriters) | ||
options.contentWriters = {}; | ||
if (typeof(options.contentHandlers) !== 'object') | ||
throw new TypeError('contentHandlers must be an object'); | ||
if (typeof(options.contentWriters) !== 'object') | ||
throw new TypeError('contentWriters must be an object'); | ||
this.routes.forEach(function(r) { | ||
if (r.matchesUrl({ url: options.url })) { | ||
if (r.methods.indexOf(method) === -1) | ||
r.methods.push(method); | ||
} | ||
}); | ||
if (!options.contentHandlers['application/json']) | ||
options.contentHandlers['application/json'] = function(body, req, res) { | ||
return JSON.parse(body); | ||
}; | ||
this.routes.push(route); | ||
return route; | ||
}; | ||
if (!options.contentHandlers['application/x-www-form-urlencoded']) | ||
options.contentHandlers['application/x-www-form-urlencoded'] = | ||
function(body, req, res) { | ||
return querystring.parse(body) || {}; | ||
}; | ||
if (!options.contentWriters['application/json']) | ||
options.contentWriters['application/json'] = function(obj, req, res) { | ||
return JSON.stringify(obj); | ||
}; | ||
Server.prototype._request = function _request(req, res, expectContinue) { | ||
var self = this; | ||
if (!options.contentWriters['application/x-www-form-urlencoded']) | ||
options.contentWriters['application/x-www-form-urlencoded'] = | ||
function(obj, req, res) { | ||
return querystring.stringify(obj) + '\n'; | ||
}; | ||
var request = new Request({ | ||
log4js: self.log4js, | ||
request: req | ||
}); | ||
var response = new Response({ | ||
log4js: self.log4js, | ||
request: request, | ||
response: res, | ||
formatters: self.formatters, | ||
expectContinue: expectContinue | ||
}); | ||
if (!options.contentHandlers['application/xml']) | ||
options.contentHandlers['application/xml'] = | ||
function(body, req, res, callback) { | ||
var parser = new xml2js.Parser(); | ||
parser.addListener('end', function(result) { | ||
if (!result) | ||
result = {}; | ||
logRequest(request); | ||
Object.keys(result).forEach(function(k) { | ||
try { | ||
if (typeof(result[k]) === 'object' && | ||
typeof(result[k]['@']) === 'object' && | ||
result[k]['@'].type && | ||
result[k]['#']) { | ||
switch (result[k]['@'].type) { | ||
case 'integer': | ||
result[k] = parseInt(result[k]['#'], 10); | ||
break; | ||
case 'boolean': | ||
result[k] = /^true$/i.test(result[k]['#']); | ||
break; | ||
default: | ||
result[k] = result[k]['#']; | ||
break; | ||
} | ||
} | ||
} catch (e) {} | ||
}); | ||
if (result.id && typeof(result.id) === 'object') | ||
delete result.id; | ||
return callback(null, result); | ||
}); | ||
parser.parseString(body); | ||
}; | ||
var route = this._findRoute(request, response); | ||
if (!route) | ||
return false; | ||
if (!options.contentWriters['application/xml']) { | ||
options.contentWriters['application/xml'] = | ||
function(obj) { | ||
assert.equal(typeof(obj), 'object'); | ||
response.serverName = this.name; | ||
return route.run(request, response); | ||
}; | ||
var res = '<?xml version="1.0" encoding="UTF-8"?>\n'; | ||
function serialize(key, val, indent) { | ||
var str = ''; | ||
Server.prototype._findRoute = function _findRoute(req, res) { | ||
assert.ok(req); | ||
assert.ok(res); | ||
switch (typeof(val)) { | ||
case 'string': | ||
case 'boolean': | ||
str += sprintf('%s<%s>%s</%s>\n', indent, key, val + '', key); | ||
break; | ||
case 'number': | ||
str += sprintf('%s<%s type="integer">%s</%s>\n', | ||
indent, key, val + '', key); | ||
break; | ||
var params; | ||
var route; | ||
var methods = []; | ||
var versions = []; | ||
case 'object': | ||
if (Array.isArray(val)) { | ||
val.forEach(function(v) { | ||
str += serialize(key, v, indent + ' '); | ||
}); | ||
} else if (val === null) { | ||
str += sprintf('%s<%s/>\n', indent, key); | ||
} else { | ||
str += sprintf('%s<%s>\n', indent, key); | ||
Object.keys(val).forEach(function(k) { | ||
str += serialize(k, val[k], indent + ' '); | ||
}); | ||
str += sprintf('%s</%s>\n', indent, key); | ||
} | ||
break; | ||
default: | ||
break; | ||
} | ||
for (var i = 0; i < this.routes.length; i++) { | ||
var r = this.routes[i]; | ||
return str; | ||
if ((params = r.matchesUrl(req))) { | ||
if (r.matchesMethod(req)) { | ||
if (r.matchesVersion(req)) { | ||
route = r; | ||
break; | ||
} else { | ||
if (r.version && versions.indexOf(r.version) === -1) | ||
versions.push(r.version); | ||
} | ||
Object.keys(obj).forEach(function(key) { | ||
res += serialize(key, obj[key], ''); | ||
}); | ||
return res; | ||
}; | ||
} | ||
if (!options.contentWriters['application/javascript']) | ||
options.contentWriters['application/javascript'] = | ||
function(obj, req, res) { | ||
var query = url.parse(req.url, true).query; | ||
var cbName = query && query.callback ? query.callback : 'callback'; | ||
var response = { | ||
code: res.code, | ||
data: obj | ||
}; | ||
res.code = 200; | ||
return cbName + '(' + JSON.stringify(response) + ');'; | ||
}; | ||
Object.keys(options.contentHandlers).forEach(function(k) { | ||
if (typeof(options.contentHandlers[k]) !== 'function') | ||
throw new TypeError('contentHandlers must be functions'); | ||
server._config.contentHandlers[k] = options.contentHandlers[k]; | ||
}); | ||
Object.keys(options.contentWriters).forEach(function(k) { | ||
if (typeof(options.contentWriters[k]) !== 'function') | ||
throw new TypeError('contentWriters must be functions'); | ||
server._config.contentWriters[k] = options.contentWriters[k]; | ||
}); | ||
// end contentHandlers/writers | ||
if (options.formatError) | ||
server._config.formatError = options.formatError; | ||
if (options.headers) { | ||
if (typeof(options.headers) !== 'object') | ||
throw new TypeError('headers must be an object'); | ||
for (k in options.headers) { | ||
if (options.headers.hasOwnProperty(k)) { | ||
if (typeof(options.headers[k]) !== 'function') | ||
throw new TypeError('headers values must be functions'); | ||
server._config.headers[k] = options.headers[k]; | ||
} | ||
} else { | ||
if (methods.indexOf(r.method) === -1) | ||
methods.push(r.method); | ||
} | ||
} | ||
} | ||
if (!server._config.formatError) | ||
server._config.formatError = function(res, e) { | ||
if (res._accept === 'application/xml') | ||
e = { error: e }; | ||
if (route) { | ||
req.params = params || {}; | ||
res.methods = route.methods; | ||
res.version = route.version; | ||
} else { | ||
res.methods = methods; | ||
res.versions = versions; | ||
return e; | ||
}; | ||
if (versions.length) { | ||
if (!this.listeners('VersionNotAllowed').length) | ||
this.once('VersionNotAllowed', defaultBadVersionHandler); | ||
if (!server._config.serverName) | ||
server._config.serverName = 'node.js'; | ||
this.emit('VersionNotAllowed', req, res, versions); | ||
} else if (methods.length) { | ||
if (!this.listeners('MethodNotAllowed').length) | ||
this.once('MethodNotAllowed', default405Handler); | ||
if (!server._config.maxRequestSize) | ||
server._config.maxRequestSize = 8192; | ||
this.emit('MethodNotAllowed', req, res, methods); | ||
} else { | ||
if (!this.listeners('NotFound').length) | ||
this.once('NotFound', default404Handler); | ||
if (!server._config.clockSkew) | ||
server._config.clockSkew = 300 * 1000; // Default 5m | ||
if (!server.sendErrorLogLevel) | ||
server._config.sendErrorLogger = log.warn; | ||
if (!server._config.acceptable) { | ||
server._config.acceptable = [ | ||
'application/json' | ||
]; | ||
this.emit('NotFound', req, res); | ||
} | ||
server._config._acceptable = {}; | ||
for (var i = 0; i < server._config.acceptable.length; i++) { | ||
var tmp = server._config.acceptable[i].split('/'); | ||
if (!server._config._acceptable[tmp[0]]) { | ||
server._config._acceptable[tmp[0]] = [tmp[1]]; | ||
} else { | ||
var found = false; | ||
for (var j = 0; j < server._config._acceptable[tmp[0]].length; j++) { | ||
if (server._config._acceptable[tmp[0]][j] === tmp[1]) { | ||
found = true; | ||
break; | ||
} | ||
} | ||
if (!found) { | ||
server._config._acceptable[tmp[0]].push(tmp[1]); | ||
} | ||
} | ||
} | ||
log.trace('server will accept types: %o', server._config.acceptable); | ||
var foundXApiVersion = false; | ||
var foundXRequestId = false; | ||
var foundXResponseTime = false; | ||
var foundContentLength = false; | ||
var foundContentType = false; | ||
var foundContentMD5 = false; | ||
var foundACAO = false; | ||
var foundACAM = false; | ||
var foundACAH = false; | ||
var foundACEH = false; | ||
for (k in server._config.headers) { | ||
if (server._config.headers.hasOwnProperty(k)) { | ||
var h = k.toLowerCase(); | ||
switch (h) { | ||
case 'x-api-version': | ||
foundXApiVersion = true; | ||
break; | ||
case 'x-request-id': | ||
foundXRequestId = true; | ||
break; | ||
case 'x-response-time': | ||
foundXResponseTime = true; | ||
break; | ||
case 'content-length': | ||
foundContentLength = true; | ||
break; | ||
case 'content-type': | ||
foundContentType = true; | ||
break; | ||
case 'content-md5': | ||
foundContentMD5 = true; | ||
break; | ||
case 'access-control-allow-origin': | ||
foundACAO = true; | ||
break; | ||
case 'access-control-allow-method': | ||
foundACAM = true; | ||
break; | ||
case 'access-control-allow-headers': | ||
foundACAH = true; | ||
break; | ||
case 'access-control-expose-headers': | ||
foundACEH = true; | ||
break; | ||
} | ||
} | ||
} | ||
if (!foundXApiVersion) { | ||
server._config.headers['X-Api-Version'] = function(res) { | ||
return res._version; | ||
}; | ||
} | ||
if (!foundXRequestId) { | ||
server._config.headers['X-Request-Id'] = function(res) { | ||
return res.requestId; | ||
}; | ||
} | ||
if (!foundXResponseTime) { | ||
server._config.headers['X-Response-Time'] = function(res) { | ||
return res._time; | ||
}; | ||
} | ||
if (!foundContentLength) { | ||
server._config.headers['Content-Length'] = function(res) { | ||
if (!res.options.noEnd && res._data) { | ||
res._bytes = Buffer.byteLength(res._data, 'utf8'); | ||
return res._bytes; | ||
} | ||
}; | ||
} | ||
if (!foundContentMD5) { | ||
server._config.headers['Content-MD5'] = function(res) { | ||
if (res._data && res.options.code !== 204) { | ||
if (!res.options.noContentMD5) { | ||
var hash = crypto.createHash('md5'); | ||
hash.update(res._data); | ||
return hash.digest('base64'); | ||
} | ||
} | ||
}; | ||
} | ||
if (!foundContentType) { | ||
server._config.headers['Content-Type'] = function(res) { | ||
if (res._data && res.options.code !== 204) | ||
return res._accept; | ||
}; | ||
} | ||
if (!foundACAO) { | ||
server._config.headers['Access-Control-Allow-Origin'] = function(res) { | ||
return '*'; | ||
}; | ||
} | ||
if (!foundACAM) { | ||
server._config.headers['Access-Control-Allow-Methods'] = function(res) { | ||
if (res._allowedMethods && res._allowedMethods.length) | ||
return res._allowedMethods.join(', '); | ||
}; | ||
} | ||
if (!foundACAH) { | ||
server._config.headers['Access-Control-Allow-Headers'] = function(res) { | ||
return [ | ||
'Accept', | ||
'Content-Type', | ||
'Content-Length', | ||
'Date', | ||
'X-Api-Version' | ||
].join(', '); | ||
}; | ||
} | ||
if (!foundACEH) { | ||
server._config.headers['Access-Control-Expose-Headers'] = function(res) { | ||
return [ | ||
'X-Api-Version', | ||
'X-Request-Id', | ||
'X-Response-Time' | ||
].join(', '); | ||
}; | ||
} | ||
return server; | ||
} | ||
return route || false; | ||
}; |
{ | ||
"author": "Mark Cavage <mcavage@gmail.com>", | ||
"name": "restify", | ||
"description": "REST framework specifically meant for web service APIs", | ||
"version": "0.5.6", | ||
"homepage": "http://mcavage.github.com/node-restify", | ||
"description": "REST framework", | ||
"version": "1.0.0-1-rc", | ||
"publishConfig": { "tag": "beta" }, | ||
"repository": { | ||
@@ -9,32 +12,25 @@ "type": "git", | ||
}, | ||
"author": "Mark Cavage <mcavage@gmail.com> (http://www.joyent.com)", | ||
"main": "lib/restify.js", | ||
"main": "lib/index.js", | ||
"directories": { | ||
"bin": "./bin", | ||
"lib": "./lib" | ||
}, | ||
"engines": { | ||
"node": ">=0.6" | ||
}, | ||
"dependencies": { | ||
"formidable": "1.0.8", | ||
"httpu": "1.0.1", | ||
"node-uuid": "1.3.1", | ||
"dtrace-provider": "0.0.5", | ||
"http-signature": "0.9.7", | ||
"mime": "1.2.4", | ||
"node-uuid": "1.2.0", | ||
"retry": "0.5.0", | ||
"semver": "1.0.13", | ||
"xml2js": "0.1.13" | ||
"sprintf": "0.1.1", | ||
"semver": "1.0.12" | ||
}, | ||
"scripts": { | ||
"pretest": "gjslint --nojsdoc -r . -x lib/sprintf.js -e node_modules", | ||
"test": "./node_modules/.bin/whiskey --quiet --sequential --timeout 4000 -t \"`find tst -name *.test.js | xargs`\"" | ||
}, | ||
"man": [ | ||
"./docs/restify.3", | ||
"./docs/restify-client.7", | ||
"./docs/restify-log.7", | ||
"./docs/restify-request.7", | ||
"./docs/restify-response.7", | ||
"./docs/restify-routes.7", | ||
"./docs/restify-throttle.7", | ||
"./docs/restify-versions.7" | ||
], | ||
"devDependencies": { | ||
"whiskey": "0.6.3" | ||
"tap": "0.1.3" | ||
}, | ||
"engines": { | ||
"node": ">=0.4" | ||
"scripts": { | ||
"test": "./node_modules/.bin/tap ./tst/*.test.js" | ||
} | ||
} |
@@ -1,46 +0,1 @@ | ||
node-restify is meant to do one thing: make it easy to build an API webservice | ||
in node.js that is correct as per the HTTP RFC. That's it. It's not MVC, it | ||
doesn't bring in a lot of baggage, it's just a small framework to let you | ||
build a web service API. | ||
## Usage | ||
var restify = require('restify'); | ||
var server = restify.createServer(); | ||
server.get('/my/:name', function(req, res) { | ||
res.send(200, { | ||
name: req.uriParams.name | ||
}); | ||
}); | ||
server.post('/my', function(req, res) { | ||
// name could be in the query string, in a form-urlencoded body, or a | ||
// JSON body | ||
res.send(201, { | ||
name: req.params.name | ||
}); | ||
}); | ||
server.del('/my/:name', function(req, res) { | ||
res.send(204); | ||
}); | ||
server.listen(8080); | ||
## Installation | ||
npm install restify | ||
## For More Information | ||
See <http://mcavage.github.com/node-restify>. | ||
## License | ||
MIT. | ||
## Bugs | ||
See <https://github.com/mcavage/node-restify/issues>. | ||
See http://mcavage.github.com/node-restify. |
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
var http = require('httpu'); | ||
var test = require('tap').test; | ||
var uuid = require('node-uuid'); | ||
var common = require('./lib/common'); | ||
var restify = require('../lib/restify'); | ||
restify.log.level(restify.LogLevel.Trace); | ||
var log4js = require('../lib/log4js_stub'); | ||
var restify = require('../lib'); | ||
// --- Globals | ||
var client = null; | ||
var server = null; | ||
var socket = '/tmp/.' + uuid(); | ||
///--- Globals | ||
var PORT = process.env.UNIT_TEST_PORT || 12345; | ||
var client; | ||
var server; | ||
// --- Tests | ||
exports.setUp = function(test, assert) { | ||
server = restify.createServer({ | ||
apiVersion: '1.2.3', | ||
serverName: 'RESTify' | ||
///--- Helpers | ||
function sendJson(req, res, next) { | ||
res.send({ | ||
hello: req.params.hello || req.params.name || null | ||
}); | ||
return next(); | ||
} | ||
function handle(req, res, next) { | ||
var code = req.params.code || 200; | ||
req.params.name = req.uriParams.name; | ||
res.send(code, req.params); | ||
} | ||
server.put('/test/:name', handle); | ||
server.post('/test/:name', handle); | ||
server.get('/test/:name', handle); | ||
server.del('/test/:name', handle); | ||
server.head('/test/:name', handle); | ||
server.head('/fail', function(req, res, next) { | ||
res.send(503); | ||
}); | ||
function sendText(req, res, next) { | ||
res.send('hello ' + (req.params.hello || req.params.name || '')); | ||
server.listen(socket, function() { | ||
client = restify.createClient({ | ||
socketPath: socket, | ||
version: '1.2.3', | ||
retryOptions: { | ||
retries: 1 | ||
} | ||
}); | ||
test.finish(); | ||
}); | ||
}; | ||
return next(); | ||
} | ||
exports.test_put_utf8 = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
expect: 200, | ||
body: { | ||
foo: 'Iñtërnâtiônàlizætiøn', | ||
code: 200 | ||
} | ||
}; | ||
client.put(req, function(err, obj) { | ||
assert.ifError(err); | ||
assert.ok(obj); | ||
assert.equal(obj.name, 'foo'); | ||
assert.equal(obj.foo, 'Iñtërnâtiônàlizætiøn'); | ||
test.finish(); | ||
}); | ||
}; | ||
exports.test_put_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
expect: 200, | ||
body: { | ||
foo: 'bar', | ||
code: 200 | ||
} | ||
}; | ||
///--- Tests | ||
client.put(req, function(err, obj) { | ||
assert.ifError(err); | ||
assert.ok(obj); | ||
assert.equal(obj.name, 'foo'); | ||
assert.equal(obj.foo, 'bar'); | ||
test.finish(); | ||
log4js.setGlobalLogLevel('TRACE'); | ||
test('setup', function(t) { | ||
server = restify.createServer({ | ||
log4js: log4js | ||
}); | ||
}; | ||
t.ok(server); | ||
server.use(restify.acceptParser(['json', 'text/plain'])); | ||
server.use(restify.dateParser()); | ||
server.use(restify.authorizationParser()); | ||
server.use(restify.queryParser()); | ||
server.use(restify.bodyParser()); | ||
exports.test_put_no_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
expect: [204], | ||
body: { | ||
foo: 'bar', | ||
code: 204 | ||
} | ||
}; | ||
server.get('/json/:name', sendJson); | ||
server.head('/json/:name', sendJson); | ||
server.put('/json/:name', sendJson); | ||
server.post('/json/:name', sendJson); | ||
client.put(req, function(err, obj) { | ||
assert.ifError(err); | ||
test.finish(); | ||
server.del('/str/:name', sendText); | ||
server.get('/str/:name', sendText); | ||
server.head('/str/:name', sendText); | ||
server.put('/str/:name', sendText); | ||
server.post('/str/:name', sendText); | ||
server.listen(PORT, '127.0.0.1', function() { | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_post_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
expect: 201, | ||
body: { | ||
foo: 'bar' | ||
}, | ||
query: { | ||
code: 201 | ||
} | ||
}; | ||
client.post(req, function(err, obj, headers) { | ||
assert.ifError(err); | ||
assert.ok(obj); | ||
assert.ok(headers); | ||
assert.equal(obj.name, 'foo'); | ||
assert.equal(obj.foo, 'bar'); | ||
test.finish(); | ||
test('create json client', function(t) { | ||
client = restify.createClient({ | ||
log4js: log4js, | ||
url: 'http://127.0.0.1:' + PORT, | ||
type: 'json' | ||
}); | ||
}; | ||
t.ok(client); | ||
t.ok(client instanceof restify.JsonClient); | ||
t.end(); | ||
}); | ||
exports.test_post_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
body: { | ||
foo: 'bar' | ||
}, | ||
query: { | ||
code: 200 | ||
} | ||
}; | ||
client.post(req, function(err, obj, headers) { | ||
assert.ifError(err); | ||
assert.ok(obj); | ||
assert.ok(headers); | ||
assert.equal(obj.name, 'foo'); | ||
assert.equal(obj.foo, 'bar'); | ||
test.finish(); | ||
test('GET json', function(t) { | ||
client.get('/json/mcavage', function(err, req, res, obj) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.equivalent(obj, {hello: 'mcavage'}); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_get_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
expect: 200, | ||
query: { | ||
foo: 'bar', | ||
code: 200 | ||
} | ||
}; | ||
client.get(req, function(err, obj) { | ||
assert.ifError(err); | ||
assert.ok(obj); | ||
assert.equal(obj.name, 'foo'); | ||
assert.equal(obj.foo, 'bar'); | ||
test.finish(); | ||
test('Check error (404)', function(t) { | ||
client.get('/' + uuid(), function(err, req, res, obj) { | ||
t.ok(err); | ||
t.ok(err.message); | ||
t.equal(err.statusCode, 404); | ||
t.ok(req); | ||
t.ok(res); | ||
t.ok(obj); | ||
t.equal(obj.code, 'ResourceNotFound'); | ||
t.ok(obj.message); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_get_no_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
query: { | ||
foo: 'bar', | ||
code: 200 | ||
} | ||
}; | ||
client.get(req, function(err, obj) { | ||
assert.ifError(err); | ||
assert.ok(obj); | ||
assert.equal(obj.name, 'foo'); | ||
assert.equal(obj.foo, 'bar'); | ||
test.finish(); | ||
test('HEAD json', function(t) { | ||
client.head('/json/mcavage', function(err, req, res) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_del_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
expect: 200, | ||
query: { | ||
code: 200 | ||
} | ||
}; | ||
client.del(req, function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
test('POST json', function(t) { | ||
client.post('/json/mcavage', { hello: 'foo' }, function(err, req, res, obj) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.equivalent(obj, {hello: 'foo'}); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_del_no_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
query: { | ||
code: 204 | ||
} | ||
}; | ||
client.del(req, function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
test('PUT json', function(t) { | ||
client.post('/json/mcavage', { hello: 'foo' }, function(err, req, res, obj) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.equivalent(obj, {hello: 'foo'}); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_head_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
expect: 200, | ||
query: { | ||
code: 200 | ||
} | ||
}; | ||
client.head(req, function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
assert.ok(headers['content-length']); | ||
test.finish(); | ||
test('create string client', function(t) { | ||
client = restify.createClient({ | ||
log4js: log4js, | ||
url: 'http://127.0.0.1:' + PORT, | ||
type: 'string' | ||
}); | ||
}; | ||
t.ok(client); | ||
t.ok(client instanceof restify.StringClient); | ||
t.end(); | ||
}); | ||
exports.test_head_no_expect = function(test, assert) { | ||
var req = { | ||
path: '/test/foo', | ||
query: { | ||
code: 204 | ||
} | ||
}; | ||
client.head(req, function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
test('GET text', function(t) { | ||
client.get('/str/mcavage', function(err, req, res, data) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.equal(res.body, data); | ||
t.equal(data, 'hello mcavage'); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_retries = function(test, assert) { | ||
var req = { | ||
path: '/fail' | ||
}; | ||
client.head(req, function(err, headers) { | ||
assert.ok(!headers); | ||
assert.ok(err); | ||
assert.equal(err.name, 'HttpError'); | ||
assert.equal(err.httpCode, 503); | ||
assert.equal(err.restCode, 'RetriesExceeded'); | ||
assert.equal(err.message, 'Maximum number of retries exceeded: 2'); | ||
test.finish(); | ||
test('HEAD text', function(t) { | ||
client.head('/str/mcavage', function(err, req, res) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_form_url_encoding = function(test, assert) { | ||
var _client = restify.createClient({ | ||
socketPath: socket, | ||
version: '1.2.3', | ||
contentType: 'application/x-www-form-urlencoded', | ||
retryOptions: { | ||
retries: 1 | ||
} | ||
test('Check error (404)', function(t) { | ||
client.get('/' + uuid(), function(err, req, res, message) { | ||
t.ok(err); | ||
t.ok(err.message); | ||
t.equal(err.statusCode, 404); | ||
t.ok(req); | ||
t.ok(res); | ||
t.ok(message); | ||
t.end(); | ||
}); | ||
}); | ||
var req = { | ||
path: '/test/foo', | ||
body: { | ||
foo: 'bar' | ||
}, | ||
query: { | ||
code: 200 | ||
} | ||
}; | ||
_client.post(req, function(err, obj, headers) { | ||
assert.ifError(err); | ||
assert.ok(obj); | ||
assert.ok(headers); | ||
assert.equal(obj.name, 'foo'); | ||
assert.equal(obj.foo, 'bar'); | ||
test.finish(); | ||
test('POST text', function(t) { | ||
client.post('/str/mcavage', 'hello=foo', function(err, req, res, data) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.equal(res.body, data); | ||
t.equal(data, 'hello foo'); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_head_string = function(test, assert) { | ||
client.head('/test/foo?code=204', function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
test('POST text (object)', function(t) { | ||
client.post('/str/mcavage', {hello: 'foo'}, function(err, req, res, data) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.equal(res.body, data); | ||
t.equal(data, 'hello foo'); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_get_string = function(test, assert) { | ||
client.get('/test/foo', function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
test('PUT text', function(t) { | ||
client.put('/str/mcavage', 'hello=foo', function(err, req, res, data) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.equal(res.body, data); | ||
t.equal(data, 'hello foo'); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_put_string = function(test, assert) { | ||
client.put('/test/foo', function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
test('DELETE text', function(t) { | ||
client.del('/str/mcavage', function(err, req, res) { | ||
t.ifError(err); | ||
t.ok(req); | ||
t.ok(res); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_post_string = function(test, assert) { | ||
client.post('/test/foo', function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
test('create raw client', function(t) { | ||
client = restify.createClient({ | ||
log4js: log4js, | ||
url: 'http://127.0.0.1:' + PORT, | ||
type: 'http', | ||
accept: 'text/plain' | ||
}); | ||
}; | ||
t.ok(client); | ||
t.ok(client instanceof restify.HttpClient); | ||
t.end(); | ||
}); | ||
exports.test_del_string = function(test, assert) { | ||
client.del('/test/foo', function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
test('GET raw', function(t) { | ||
client.get('/str/mcavage', function(connectErr, req) { | ||
t.ifError(connectErr); | ||
t.ok(req); | ||
req.on('result', function(err, res) { | ||
t.ifError(err); | ||
res.body = ''; | ||
res.setEncoding('utf8'); | ||
res.on('data', function(chunk) { | ||
res.body += chunk; | ||
}); | ||
res.on('end', function() { | ||
t.equal(res.body, 'hello mcavage'); | ||
t.end(); | ||
}); | ||
}); | ||
}); | ||
}; | ||
}); | ||
exports.test_url_in_constructor = function(test, assert) { | ||
var client = restify.createClient({ | ||
path: '/test/foo', | ||
socketPath: socket, | ||
version: '1.2.3', | ||
retryOptions: { | ||
retries: 1 | ||
test('POST raw', function(t) { | ||
var opts = { | ||
path: '/str/mcavage', | ||
headers: { | ||
'content-type': 'application/x-www-form-urlencoded' | ||
} | ||
}); | ||
}; | ||
client.post(opts, function(connectErr, req) { | ||
t.ifError(connectErr); | ||
client.del(function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
}); | ||
}; | ||
req.write('hello=snoopy'); | ||
req.end(); | ||
req.on('result', function(err, res) { | ||
t.ifError(err); | ||
res.body = ''; | ||
res.setEncoding('utf8'); | ||
res.on('data', function(chunk) { | ||
res.body += chunk; | ||
}); | ||
exports.test_url_prefix = function(test, assert) { | ||
var client = restify.createClient({ | ||
path: '/test', | ||
socketPath: socket, | ||
version: '1.2.3', | ||
retryOptions: { | ||
retries: 1 | ||
} | ||
res.on('end', function() { | ||
t.equal(res.body, 'hello snoopy'); | ||
t.end(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
client.del({ path: '/foo' }, function(err, headers) { | ||
assert.ifError(err); | ||
assert.ok(headers); | ||
test.finish(); | ||
test('teardown', function(t) { | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.tearDown = function(test, assert) { | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
}; | ||
@@ -1,553 +0,453 @@ | ||
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
var fs = require('fs'); | ||
var http = require('httpu'); | ||
var https = require('httpu'); | ||
var uuid = require('node-uuid'); | ||
// Copyright 2011 Mark Cavage, Inc. All rights reserved. | ||
var common = require('./lib/common'); | ||
var restify = require('../lib/restify'); | ||
var http = require('http'); | ||
var newError = restify.newError; | ||
var log = restify.log; | ||
log.level(log.Level.Trace); | ||
var d = require('dtrace-provider'); | ||
var test = require('tap').test; | ||
var uuid = require('node-uuid'); | ||
var HttpError = require('../lib/errors').HttpError; | ||
var RestError = require('../lib/errors').RestError; | ||
var log4js = require('../lib/log4js_stub'); | ||
var Request = require('../lib/request'); | ||
var Response = require('../lib/response'); | ||
var Server = require('../lib/server'); | ||
// --- Globals | ||
var socket = '/tmp/.' + uuid(); | ||
///--- Globals | ||
var DTRACE = d.createDTraceProvider('restifyUnitTest'); | ||
var PORT = process.env.UNIT_TEST_PORT || 12345; | ||
var _handler = function(req, res, next) { | ||
res.send(200); | ||
return next(); | ||
}; | ||
///--- Tests | ||
// --- Helpers | ||
test('throws on missing options', function(t) { | ||
t.throws(function() { | ||
return new Server(); | ||
}, new TypeError('options (Object) required')); | ||
t.end(); | ||
}); | ||
function _pad(val) { | ||
if (parseInt(val, 10) < 10) { | ||
val = '0' + val; | ||
} | ||
return val; | ||
} | ||
test('throws on missing log4js', function(t) { | ||
t.throws(function() { | ||
return new Server({}); | ||
}, new TypeError('options.dtrace (Object) required')); | ||
t.end(); | ||
}); | ||
function _rfc822(date) { | ||
var months = ['Jan', | ||
'Feb', | ||
'Mar', | ||
'Apr', | ||
'May', | ||
'Jun', | ||
'Jul', | ||
'Aug', | ||
'Sep', | ||
'Oct', | ||
'Nov', | ||
'Dec']; | ||
var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; | ||
return days[date.getUTCDay()] + ', ' + | ||
_pad(date.getUTCDate()) + ' ' + | ||
months[date.getUTCMonth()] + ' ' + | ||
date.getUTCFullYear() + ' ' + | ||
_pad(date.getUTCHours()) + ':' + | ||
_pad(date.getUTCMinutes()) + ':' + | ||
_pad(date.getUTCSeconds()) + | ||
' GMT'; | ||
} | ||
test('throws on missing log4js', function(t) { | ||
t.throws(function() { | ||
return new Server({ dtrace: {} }); | ||
}, new TypeError('options.log4js (Object) required')); | ||
t.end(); | ||
}); | ||
// --- Tests | ||
test('ok', function(t) { | ||
t.ok(new Server({ dtrace: DTRACE, log4js: log4js })); | ||
t.end(); | ||
}); | ||
exports.test_create_no_options = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
server.get('/', _handler); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.headers.server, 'node.js'); | ||
assert.equal(res.statusCode, 200); | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
}).end(); | ||
}); | ||
}; | ||
test('ok (ssl)', function(t) { | ||
// Lame, just make sure we go down the https path | ||
try { | ||
t.ok(new Server({ | ||
dtrace: DTRACE, | ||
log4js: log4js, | ||
certificate: 'hello', | ||
key: 'world' | ||
})); | ||
t.fail('HTTPS server not created'); | ||
} catch (e) { | ||
// noop | ||
} | ||
t.end(); | ||
}); | ||
exports.test_create_empty_options = function(test, assert) { | ||
var server = restify.createServer({}); | ||
var socket = '/tmp/.' + uuid(); | ||
server.get('/', _handler); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.headers.server, 'node.js'); | ||
assert.equal(res.statusCode, 200); | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
}).end(); | ||
test('listen and close (port only)', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.listen(PORT, function() { | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}); | ||
}; | ||
}); | ||
exports.test_server_name = function(test, assert) { | ||
var server = restify.createServer({ | ||
serverName: 'foo' | ||
test('listen and close (port and hostname)', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.listen(PORT, '127.0.0.1', function() { | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}); | ||
var socket = '/tmp/.' + uuid(); | ||
}); | ||
server.get('/', _handler); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.headers.server, 'foo'); | ||
assert.equal(res.statusCode, 200); | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
}).end(); | ||
test('listen and close (socketPath)', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.listen('/tmp/.' + uuid(), function() { | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}); | ||
}; | ||
}); | ||
exports.test_max_request_size = function(test, assert) { | ||
var server = restify.createServer({ | ||
maxRequestSize: 5 | ||
test('get (path only)', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.get('/foo/:id', function tester(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
res.send(); | ||
return next(); | ||
}); | ||
var socket = '/tmp/.' + uuid(); | ||
server.post('/', _handler); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
opts.method = 'POST'; | ||
var req = http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 413); | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
var done = 0; | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
agent: false | ||
}; | ||
http.get(opts, function(res) { | ||
t.equal(res.statusCode, 200); | ||
if (++done == 2) { | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
} | ||
}); | ||
req.write(JSON.stringify({ ThisIsALongString: uuid()}, null, 2)); | ||
req.end(); | ||
}); | ||
}; | ||
exports.test_clock_ok = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
server.get('/', _handler); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
opts.headers.Date = _rfc822(new Date()); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
server.on('close', function() { | ||
test.finish(); | ||
server.on('after', function(req, res) { | ||
t.ok(req); | ||
t.ok(res); | ||
if (++done == 2) { | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
server.close(); | ||
}).end(); | ||
} | ||
}); | ||
}; | ||
}); | ||
exports.test_clock_skew = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
test('get (path and version ok)', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.get({ | ||
url: '/foo/:id', | ||
version: '1.2.3' | ||
}, function tester(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
res.send(); | ||
return next(); | ||
}); | ||
server.get('/', _handler); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
opts.headers.Date = _rfc822(new Date(1995, 11, 17, 3, 24, 0)); | ||
var done = 0; | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
agent: false, | ||
headers: { | ||
'accept-version': '~1.2' | ||
} | ||
}; | ||
http.get(opts, function(res) { | ||
t.equal(res.statusCode, 200); | ||
if (++done == 2) { | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
} | ||
}); | ||
}); | ||
http.request(opts, function(res) { | ||
res._skipAllowedMethods = true; | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 400); | ||
server.on('close', function() { | ||
test.finish(); | ||
server.on('after', function(req, res) { | ||
t.ok(req); | ||
t.ok(res); | ||
if (++done == 2) { | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
server.close(); | ||
}).end(); | ||
} | ||
}); | ||
}; | ||
}); | ||
exports.test_regex_route = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
test('get (path and version not ok)', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.get({ | ||
url: '/foo/:id', | ||
version: '1.2.3' | ||
}, function(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
res.send(); | ||
return next(); | ||
}); | ||
server.get(/^\/users?(?:\/(\d+)(?:\.\.(\d+))?)?/, _handler); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/users/1..15'); | ||
server.get({ | ||
url: '/foo/:id', | ||
version: '1.2.4' | ||
}, function(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
res.send(); | ||
return next(); | ||
}); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
server.on('close', function() { | ||
test.finish(); | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
agent: false, | ||
headers: { | ||
'accept': 'text/plain', | ||
'accept-version': '~2.1' | ||
} | ||
}; | ||
http.get(opts, function(res) { | ||
t.equal(res.statusCode, 400); | ||
res.setEncoding('utf8'); | ||
res.body = ''; | ||
res.on('data', function(chunk) { | ||
res.body += chunk; | ||
}); | ||
server.close(); | ||
}).end(); | ||
res.on('end', function() { | ||
t.equal(res.body, 'GET /foo/bar supports versions: 1.2.3, 1.2.4'); | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}; | ||
}); | ||
exports.test_create_ssl = function(test, assert) { | ||
var server = restify.createServer({ | ||
cert: fs.readFileSync(__dirname + '/test_cert.pem', 'ascii'), | ||
key: fs.readFileSync(__dirname + '/test_key.pem', 'ascii') | ||
test('use + get (path only)', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
var handler = 0; | ||
server.use(function(req, res, next) { | ||
handler++; | ||
return next(); | ||
}); | ||
assert.ok(server); | ||
assert.ok(server.cert); | ||
assert.ok(server.key); | ||
server.get('/', function(req, res, next) { res.send(200); return next(); }); | ||
server.listen(socket, function() { | ||
// Can't actually drive requests over httpu for SSL. | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
server.get('/foo/:id', function tester(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
handler++; | ||
res.send(); | ||
return next(); | ||
}); | ||
}; | ||
exports.test_abort_pre_send = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
server.get('/', | ||
[function(req, res, next) { | ||
res.send(200); | ||
return next(); | ||
}], | ||
function(req, res, next) { | ||
assert.ok(false, 'FAIL! main handler invoked'); | ||
}, | ||
[function(req, res, next) { | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
}]); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
}).end(); | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
agent: false | ||
}; | ||
http.get(opts, function(res) { | ||
t.equal(res.statusCode, 200); | ||
t.equal(handler, 2); | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}); | ||
}); | ||
}; | ||
}); | ||
exports.test_abort_pre_error = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
server.get('/', | ||
[function(req, res, next) { | ||
res.sendError(newError()); | ||
return next(); | ||
}], | ||
function(req, res, next) { | ||
assert.ok(false, 'FAIL! main handler invoked'); | ||
}, | ||
[function(req, res, next) { | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
}]); | ||
test('rm', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 500); | ||
}).end(); | ||
server.get('/foo/:id', function(req, res, next) { | ||
return next(); | ||
}); | ||
}; | ||
exports.test_abort_main_error = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
server.get('/', | ||
[function(req, res, next) { | ||
return next(); | ||
}], | ||
function(req, res, next) { | ||
res.sendError(newError()); | ||
return next(); | ||
}, | ||
function(req, res, next) { | ||
assert.ok(false, 'FAIL! main handler invoked'); | ||
}, | ||
[function(req, res, next) { | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
}]); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 500); | ||
}).end(); | ||
server.get('/bar/:id', function(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'foo'); | ||
res.send(); | ||
return next(); | ||
}); | ||
}; | ||
t.ok(server.rm('GET /foo/:id')); | ||
exports.test_main_no_abort = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
var called = false; | ||
server.get('/', | ||
[function(req, res, next) { | ||
return next(); | ||
}], | ||
function(req, res, next) { | ||
res.send(200); | ||
return next(); | ||
}, | ||
function(req, res, next) { | ||
called = true; | ||
return next(); | ||
}, | ||
[function(req, res, next) { | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
assert.ok(called); | ||
server.close(); | ||
}]); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
}).end(); | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
agent: false | ||
}; | ||
http.get(opts, function(res) { | ||
t.equal(res.statusCode, 404); | ||
opts.path = '/bar/foo'; | ||
http.get(opts, function(res2) { | ||
t.equal(res2.statusCode, 200); | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}; | ||
}); | ||
exports.test_gh_27 = function(test, assert) { | ||
var server = restify.createServer(); | ||
test('405', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
var called = false; | ||
function before(req, res, next) { | ||
function anything() { | ||
return next(); | ||
} | ||
return anything(); | ||
} | ||
server.get('/foo', [before], function(req, res, next) { | ||
called = true; | ||
res.send(200); | ||
server.post('/foo/:id', function tester(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
res.send(); | ||
return next(); | ||
}); | ||
var socket = '/tmp/.' + uuid(); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/foo'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
server.on('close', function() { | ||
test.finish(); | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
agent: false | ||
}; | ||
http.get(opts, function(res) { | ||
t.equal(res.statusCode, 405); | ||
t.equal(res.headers.allow, 'POST'); | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
assert.ok(called); | ||
server.close(); | ||
}).end(); | ||
}); | ||
}); | ||
}; | ||
}); | ||
exports.test_custom_content = function(test, assert) { | ||
var server = restify.createServer({ | ||
contentHandlers: { | ||
'application/foo': function(body) { | ||
assert.ok(body); | ||
test('PUT ok', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
return JSON.parse(body); | ||
} | ||
}, | ||
contentWriters: { | ||
'application/foo': function(obj) { | ||
assert.ok(obj); | ||
return JSON.stringify(obj); | ||
} | ||
}, | ||
accept: ['application/json', 'application/foo'] | ||
}); | ||
server.post('/custom_content', function(req, res, next) { | ||
assert.equal(req.params.json, 'foo'); | ||
res.send(200, {foo: 'bar'}); | ||
server.put('/foo/:id', function tester(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
res.send(); | ||
return next(); | ||
}); | ||
var socket = '/tmp/.' + uuid(); | ||
server.listen(socket, function() { | ||
var content = JSON.stringify({json: 'foo'}); | ||
var opts = common.newOptions(socket, '/custom_content'); | ||
opts.method = 'POST'; | ||
opts.headers.Accept = 'application/foo'; | ||
opts.headers['Content-Type'] = 'application/foo'; | ||
opts.headers['Content-Length'] = content.length; | ||
var req = http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
common.checkContent(assert, res, function() { | ||
assert.equal(res.params.foo, 'bar'); | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.close(); | ||
}, 'application/foo'); | ||
}); | ||
req.write(content); | ||
req.end(); | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
method: 'PUT', | ||
agent: false | ||
}; | ||
http.request(opts, function(res) { | ||
t.equal(res.statusCode, 200); | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}).end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_custom_headers = function(test, assert) { | ||
var server = restify.createServer({ | ||
headers: { | ||
'access-control-allow-headers': function(res) { | ||
return [ | ||
'x-unit-test' | ||
].join(', '); | ||
}, | ||
'X-Unit-Test': function(res) { | ||
return 'foo'; | ||
} | ||
} | ||
}); | ||
test('HEAD ok', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.get('/custom_headers', function(req, res, next) { | ||
res.send(200); | ||
server.head('/foo/:id', function tester(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
res.send('hi there'); | ||
return next(); | ||
}); | ||
var socket = '/tmp/.' + uuid(); | ||
server.listen(socket, function() { | ||
var content = JSON.stringify({json: 'foo'}); | ||
var opts = common.newOptions(socket, '/custom_headers'); | ||
var req = http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
console.log(res.headers); | ||
assert.equal(res.headers['access-control-allow-headers'], 'x-unit-test'); | ||
assert.equal(res.headers['x-unit-test'], 'foo'); | ||
server.on('close', function() { | ||
test.finish(); | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
method: 'HEAD', | ||
agent: false | ||
}; | ||
http.request(opts, function(res) { | ||
t.equal(res.statusCode, 200); | ||
res.on('data', function(chunk) { | ||
t.fail('Data was sent on HEAD'); | ||
}); | ||
server.close(); | ||
}); | ||
req.write(content); | ||
req.end(); | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}).end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_send_error = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
test('DELETE ok', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.get('/foo', function(req, res, next) { | ||
return next(newError()); | ||
server.del('/foo/:id', function tester(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
res.send(204, 'hi there'); | ||
return next(); | ||
}); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/users/1..15'); | ||
http.get({ | ||
path: '/foo', | ||
socketPath: socket | ||
}, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 500); | ||
server.on('close', function() { | ||
test.finish(); | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
method: 'DELETE', | ||
agent: false | ||
}; | ||
http.request(opts, function(res) { | ||
t.equal(res.statusCode, 204); | ||
res.on('data', function(chunk) { | ||
t.fail('Data was sent on 204'); | ||
}); | ||
server.close(); | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}).end(); | ||
}); | ||
}; | ||
}); | ||
// GH-49 | ||
exports.test_route_with_content_type_suffix = function(test, assert) { | ||
var server = restify.createServer(); | ||
var socket = '/tmp/.' + uuid(); | ||
test('OPTIONS', function(t) { | ||
var server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
server.get('/:foo', function(req, res, next) { | ||
assert.ok(req.uriParams.foo); | ||
var found = false; | ||
if (req.uriParams.foo === 'blah' || req.uriParams.foo === 'mark.cavage') | ||
found = true; | ||
assert.ok(found); | ||
res.send(202); | ||
server.get('/foo/:id', function tester(req, res, next) { | ||
t.ok(req.params); | ||
t.equal(req.params.id, 'bar'); | ||
res.send(); | ||
return next(); | ||
}); | ||
server.on('close', function() { | ||
test.finish(); | ||
}); | ||
server.listen(socket, function() { | ||
var opts = common.newOptions(socket, '/blah.xml'); | ||
server.listen(PORT, function() { | ||
var opts = { | ||
hostname: 'localhost', | ||
port: PORT, | ||
path: '/foo/bar', | ||
method: 'OPTIONS', | ||
agent: false | ||
}; | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 202); | ||
opts = common.newOptions(socket, '/mark.cavage'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 202); | ||
server.close(); | ||
}).end(); | ||
t.equal(res.statusCode, 200); | ||
t.ok(res.headers.allow); | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
}).end(); | ||
}); | ||
}; | ||
}); |
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
var http = require('httpu'); | ||
var http = require('http'); | ||
var d = require('dtrace-provider'); | ||
var test = require('tap').test; | ||
var uuid = require('node-uuid'); | ||
var common = require('./lib/common'); | ||
var restify = require('../lib/restify'); | ||
restify.log.level(restify.LogLevel.Trace); | ||
var createClient = require('../lib').createClient; | ||
var HttpError = require('../lib/errors').HttpError; | ||
var RestError = require('../lib/errors').RestError; | ||
var log4js = require('../lib/log4js_stub'); | ||
var Request = require('../lib/request'); | ||
var Response = require('../lib/response'); | ||
var Server = require('../lib/server'); | ||
var throttle = require('../lib/plugins/throttle'); | ||
// --- Globals | ||
var options = {}; | ||
var server = null; | ||
var socket = '/tmp/.' + uuid(); | ||
///--- Globals | ||
var DTRACE = d.createDTraceProvider('throttleUnitTest'); | ||
var PORT = process.env.UNIT_TEST_PORT || 12345; | ||
var client; | ||
var server; | ||
var username = uuid(); | ||
@@ -20,11 +31,16 @@ var password = uuid(); | ||
// --- Tests | ||
//--- Tests | ||
exports.setUp = function(test, assert) { | ||
server = restify.createServer({ | ||
apiVersion: '1.2.3', | ||
serverName: 'RESTify' | ||
test('setup', function(t) { | ||
server = new Server({ dtrace: DTRACE, log4js: log4js }); | ||
t.ok(server); | ||
server.use(function(req, res, next) { | ||
if (req.params.name) | ||
req.username = req.params.name; | ||
return next(); | ||
}); | ||
var throttle = restify.createThrottle({ | ||
server.use(throttle({ | ||
burst: 1, | ||
@@ -43,100 +59,92 @@ rate: 0.5, | ||
} | ||
})); | ||
server.get('/test/:name', function(req, res, next) { | ||
res.send(); | ||
return next(); | ||
}); | ||
server.get('/test/:name', | ||
function(req, res, next) { | ||
req.username = req.uriParams.name; | ||
return next(); | ||
}, | ||
throttle, | ||
function(req, res, next) { | ||
res.send(200); | ||
return next(); | ||
} | ||
); | ||
server.listen(socket, function() { | ||
test.finish(); | ||
server.listen(PORT, '127.0.0.1', function() { | ||
client = createClient({ | ||
dtrace: DTRACE, | ||
log4js: log4js, | ||
name: 'throttleUnitTest', | ||
type: 'string', | ||
url: 'http://127.0.0.1:' + PORT | ||
}); | ||
t.ok(client); | ||
t.end(); | ||
}); | ||
}; | ||
}); | ||
exports.test_ok = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/throttleMe'); | ||
opts.method = 'GET'; | ||
test('ok', function(t) { | ||
client.get('/test/throttleMe', function(err, req, res, body) { | ||
t.ifError(err); | ||
t.equal(res.statusCode, 200); | ||
t.end(); | ||
}); | ||
}); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
test.finish(); | ||
}).end(); | ||
}; | ||
test('throttled', function(t) { | ||
client.get('/test/throttleMe', function(err, req, res, body) { | ||
t.ok(err); | ||
t.equal(err.statusCode, 429); | ||
t.ok(err.message); | ||
t.equal(res.statusCode, 429); | ||
setTimeout(function() { t.end(); }, 2100); | ||
}); | ||
}); | ||
exports.test_throttled = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/throttleMe'); | ||
opts.method = 'GET'; | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 420); | ||
common.checkContent(assert, res, function() { | ||
assert.ok(res.params); | ||
assert.equal(res.params.code, 'RequestThrottled'); | ||
assert.ok(res.params.message); | ||
setTimeout(function() { test.finish(); }, 2100); | ||
}); | ||
}).end(); | ||
}; | ||
test('ok after tokens', function(t) { | ||
client.get('/test/throttleMe', function(err, req, res, body) { | ||
t.ifError(err); | ||
t.equal(res.statusCode, 200); | ||
t.end(); | ||
}); | ||
}); | ||
exports.test_ok_after_window = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/throttleMe'); | ||
opts.method = 'GET'; | ||
test('override limited', function(t) { | ||
client.get('/test/special', function(err, req, res, body) { | ||
t.ifError(err); | ||
t.equal(res.statusCode, 200); | ||
t.end(); | ||
}); | ||
}); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
test.finish(); | ||
}).end(); | ||
}; | ||
test('override limited (not throttled)', function(t) { | ||
client.get('/test/special', function(err, req, res, body) { | ||
t.ifError(err); | ||
t.equal(res.statusCode, 200); | ||
t.end(); | ||
}); | ||
}); | ||
exports.test_override_limited = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/special'); | ||
opts.method = 'GET'; | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
test.finish(); | ||
}).end(); | ||
}).end(); | ||
}; | ||
test('override unlimited', function(t) { | ||
client.get('/test/admin', function(err, req, res, body) { | ||
t.ifError(err); | ||
t.equal(res.statusCode, 200); | ||
t.end(); | ||
}); | ||
}); | ||
exports.test_override_unlimited = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/admin'); | ||
opts.method = 'GET'; | ||
test('override unlimited (not throttled)', function(t) { | ||
client.get('/test/admin', function(err, req, res, body) { | ||
t.ifError(err); | ||
t.equal(res.statusCode, 200); | ||
t.end(); | ||
}); | ||
}); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
test.finish(); | ||
}).end(); | ||
}).end(); | ||
}; | ||
exports.tearDown = function(test, assert) { | ||
server.on('close', function() { | ||
test.finish(); | ||
test('teardown', function(t) { | ||
server.close(function() { | ||
t.end(); | ||
}); | ||
server.close(); | ||
}; | ||
}); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
11
179820
7
40
4345
2
+ Addeddtrace-provider@0.0.5
+ Addedhttp-signature@0.9.7
+ Addedmime@1.2.4
+ Addedsprintf@0.1.1
+ Addedasn1@0.1.9(transitive)
+ Addedctype@0.3.1(transitive)
+ Addeddtrace-provider@0.0.5(transitive)
+ Addedhttp-signature@0.9.7(transitive)
+ Addedmime@1.2.4(transitive)
+ Addednode-uuid@1.2.0(transitive)
+ Addedsemver@1.0.12(transitive)
+ Addedsprintf@0.1.1(transitive)
- Removedformidable@1.0.8
- Removedhttpu@1.0.1
- Removedxml2js@0.1.13
- Removedformidable@1.0.8(transitive)
- Removedhttpu@1.0.1(transitive)
- Removednode-uuid@1.3.1(transitive)
- Removedsax@1.4.1(transitive)
- Removedsemver@1.0.13(transitive)
- Removedxml2js@0.1.13(transitive)
Updatednode-uuid@1.2.0
Updatedsemver@1.0.12