diff --git a/CHANGES.md b/CHANGES.md index cee98ded0..29dcc143b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,36 @@ # restify Changelog -## 1.4.2 (not yet released) - +## 1.4.4 (not yet released) + +- GH-177 move to dtrace-provider 0.0.9 +- GH-172 option `keepExtensions` in body parser (Luri Aranda) +- GH-173 set req.files on multipart (Jonathan Wiepert) +- GH-169 json body parser failing content-md5, sometimes +- GH-166 plugins assume content-length is integer (Petter Rasmussen) +- GH-164 Get rid of ETag quotes and W/ prefix for ETag header (Dominic Denicola) +- GH-163 Don't default content-length to 0 +- GH-160 don't rely on npm's directories: "bin". +- GH-159 req.charset not working (Tamas Daniel) +- set `req.params.files` on multipart file uploads + +## 1.4.3 + +- update dependencies to latest (notably dtrace-provider) +- GH-158 res.charSet broken (Tamas Daniel) +- GH-154 bodyParser work with PATCH (Domenic Denicola) +- GH-153 bodyParser can reject or allow unknown content-types (Domenic Denicola) +- GH-152 Send JSON on HttpError (Domenic Denicola) +- GH-149 allow setting of max body size (and return 413) (Simon Sturmer) +- GH-146 allow setting of route regex flags when path is not a RegExp +- Support SSL CAs (Paul Bouzakis) +- body parser should return 415 when content-type not known (Simon Sturmer) + + +## 1.4.2 + +- Add Route.realize( Domenic Denicola) +- defaultResponseHeaders setter was setting the wrong method (Harry Marr) +- Workaround joyent/node#3257 (Dave Pacheco) - logging typo (Pedro Candel) - response `beforeSend` event (Paul Bouzakis) diff --git a/docs/index.restdown b/docs/index.restdown index cde73be68..f0e0ef2f3 100644 --- a/docs/index.restdown +++ b/docs/index.restdown @@ -348,7 +348,7 @@ HTTP status codes as a subclass of `HttpError`. So, for example, you can do this: server.get('/hello/:name', function(req, res, next) { - return next(new restify.ConflictError("I just don't like you")); + return next(new restify.InvalidArgumentError("I just don't like you")); }); $ curl -is -H 'accept: text/*' localhost:8080/hello/mark @@ -647,10 +647,11 @@ supported. server.use(restify.bodyParser({ mapParams: false })); -You can pass in an options object; currently the only parameter honored is -`mapParams`, which you can set to `false` to disable copying k/v pairs from +You can pass in an options object; currently the only two parameters honored are +`mapParams` and `keepExtensions`. You can set `mapParams` to `false` to disable copying k/v pairs from the request body into `req.params`; instead, `req.body` will be overwritten with -the parsed object. The default is to map params into `req.params`. +the parsed object. The default is to map params into `req.params`. The `keepExtensions` option is +useful if you want the uploaded files to include the extensions of the original files. Default is `false`. ### Throttle diff --git a/lib/clients/json_client.js b/lib/clients/json_client.js index 23bc08591..eb95247f2 100644 --- a/lib/clients/json_client.js +++ b/lib/clients/json_client.js @@ -31,7 +31,7 @@ module.exports = JsonClient; JsonClient.prototype.write = function write(options, body, callback) { if (typeof (options) !== 'object') throw new TypeError('options (Object) required'); - if (body !== null && typeof (body) !== 'object') + if (!body || typeof (body) !== 'object') throw new TypeError('body (Object) required'); if (typeof (callback) !== 'function') throw new TypeError('callback (Function) required'); @@ -50,7 +50,7 @@ JsonClient.prototype.parse = function parse(req, callback) { return this._super.parse.call(this, req, function (err, req, res, data) { var obj; try { - if (data) { + if (data && !/^\s*$/.test(data)) { obj = JSON.parse(data); } else { obj = {}; diff --git a/lib/clients/string_client.js b/lib/clients/string_client.js index 4d15e11f5..164a3f4f0 100644 --- a/lib/clients/string_client.js +++ b/lib/clients/string_client.js @@ -98,10 +98,11 @@ StringClient.prototype.write = function write(options, body, callback) { if (body) { self.log.trace('sending body -> %s', body); - req.write(body); + req.end(body); + } else { + req.end(); } - req.end(); return req.once('result', self.parse(req, callback)); }); }; diff --git a/lib/errors/http_error.js b/lib/errors/http_error.js index 99d906429..fc2e010dd 100644 --- a/lib/errors/http_error.js +++ b/lib/errors/http_error.js @@ -53,7 +53,7 @@ function HttpError(code, message, body, constructorOpt) { code = parseInt(code, 10); this.message = message || ''; - this.body = body || message || ''; + this.body = body || (message ? { message: message } : ''); this.statusCode = this.httpCode = code; } util.inherits(HttpError, Error); diff --git a/lib/index.js b/lib/index.js index 6f1ef2d94..1564d6b8b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -10,6 +10,7 @@ var clients = require('./clients'); var errors = require('./errors'); var plugins = require('./plugins'); +var sanitizePath = require('./utils').sanitizePath; ///--- Globals @@ -177,6 +178,21 @@ module.exports = { }, + /** + * Returns a string representation of a URL pattern , with its parameters + * filled in by the passed hash. + * + * If a key is not found in the hash for a param, it is left alone. + * + * @param {Object} a hash of parameter names to values for substitution. + */ + realizeUrl: function realizeUrl(pattern, params) { + return sanitizePath(pattern.replace(/\/:([^/]+)/g, function (match, key) { + return params.hasOwnProperty(key) ? '/' + params[key] : match; + })); + }, + + HttpClient: HttpClient, JsonClient: JsonClient, StringClient: StringClient @@ -200,5 +216,5 @@ module.exports.__defineSetter__('defaultResponseHeaders', function (f) { throw new TypeError('defaultResponseHeaders must be a function'); } - http.ServerResponse.prototype.defaultHeaders = f; + http.ServerResponse.prototype.defaultResponseHeaders = f; }); diff --git a/lib/plugins/body_parser.js b/lib/plugins/body_parser.js index f65e08326..08f33daf7 100644 --- a/lib/plugins/body_parser.js +++ b/lib/plugins/body_parser.js @@ -4,6 +4,10 @@ var jsonParser = require('./json_body_parser'); var formParser = require('./form_body_parser'); var multipartParser = require('./multipart_parser'); +var errors = require('../errors'); + +var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError; + function bodyParser(options) { @@ -12,6 +16,9 @@ function bodyParser(options) { var parseMultipart = multipartParser(options); return function parseBody(req, res, next) { + if (req.method !== 'POST' && req.method !== 'PUT' && req.method !== 'PATCH') + return next(); + if (req.contentLength === 0 && !req.chunked) return next(); @@ -21,6 +28,9 @@ function bodyParser(options) { return parseForm(req, res, next); } else if (req.contentType === 'multipart/form-data') { return parseMultipart(req, res, next); + } else if (options && options.rejectUnknown !== false) { + return next(new UnsupportedMediaTypeError('Unsupported Content-Type: ' + + req.contentType)); } return next(); diff --git a/lib/plugins/conditional_request.js b/lib/plugins/conditional_request.js index d2087a4b2..ac302549c 100644 --- a/lib/plugins/conditional_request.js +++ b/lib/plugins/conditional_request.js @@ -34,6 +34,9 @@ function conditionalRequest() { var matched = false; if (typeof (etag) === 'string' && etag.length !== 0) { + etag = + etag.replace(/^W\//, '').replace(/^"(\w*)"$/, '$1'); + if (req.headers['if-match']) { // RFC: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24 diff --git a/lib/plugins/form_body_parser.js b/lib/plugins/form_body_parser.js index d9a8d48ba..f7a50a00b 100644 --- a/lib/plugins/form_body_parser.js +++ b/lib/plugins/form_body_parser.js @@ -11,6 +11,7 @@ var errors = require('../errors'); var BadDigestError = errors.BadDigestError; var InvalidContentError = errors.InvalidContentError; +var RequestEntityTooLargeError = errors.RequestEntityTooLargeError; @@ -41,10 +42,13 @@ function urlEncodedBodyParser(options) { if (req.header('content-md5')) hash = crypto.createHash('md5'); + var bytesReceived = 0, maxBodySize = options.maxBodySize || 0; req.body = ''; - req.setEncoding('utf8'); req.on('data', function (chunk) { - req.body += chunk; + bytesReceived += chunk.length; + if (maxBodySize && bytesReceived > maxBodySize) + return; + req.body += chunk.toString('utf8'); if (hash) hash.update(chunk); }); @@ -52,6 +56,10 @@ function urlEncodedBodyParser(options) { return next(err); }); req.on('end', function () { + if (maxBodySize && bytesReceived > maxBodySize) + return next(new RequestEntityTooLargeError('Request body size exceeds ' + + maxBodySize)); + if (!req.body) return next(); diff --git a/lib/plugins/json_body_parser.js b/lib/plugins/json_body_parser.js index 590b1e403..d2341a2cc 100644 --- a/lib/plugins/json_body_parser.js +++ b/lib/plugins/json_body_parser.js @@ -10,6 +10,7 @@ var errors = require('../errors'); var BadDigestError = errors.BadDigestError; var InvalidContentError = errors.InvalidContentError; +var RequestEntityTooLargeError = errors.RequestEntityTooLargeError; @@ -17,7 +18,7 @@ var InvalidContentError = errors.InvalidContentError; /** * Returns a plugin that will parse the HTTP request body IFF the - * contentType is application/x-www-form-urlencoded. + * contentType is application/json. * * If req.params already contains a given key, that key is skipped and an * error is logged. @@ -40,22 +41,32 @@ function jsonBodyParser(options) { if (req.header('content-md5')) hash = crypto.createHash('md5'); + var bytesReceived = 0, maxBodySize = options.maxBodySize || 0; req.body = ''; - req.setEncoding('utf8'); req.on('data', function (chunk) { - req.body += chunk; - if (hash) - hash.update(chunk); + bytesReceived += chunk.length; + if (maxBodySize && bytesReceived > maxBodySize) + return; + req.body += chunk.toString('utf8'); }); req.on('error', function (err) { return next(err); }); req.on('end', function () { + if (maxBodySize && bytesReceived > maxBodySize) + return next(new RequestEntityTooLargeError('Request body size exceeds ' + + maxBodySize)); + if (!req.body) return next(); - if (hash && req.header('content-md5') !== hash.digest('base64')) - return next(new BadDigestError('Content-MD5 did not match')); + if (hash) { + hash.update(req.body); + var digest = hash.digest('base64'); + if (req.header('content-md5') !== digest) { + return next(new BadDigestError('Content-MD5 did not match')); + } + } try { var params = JSON.parse(req.body); diff --git a/lib/plugins/multipart_parser.js b/lib/plugins/multipart_parser.js index 27e186928..670387255 100644 --- a/lib/plugins/multipart_parser.js +++ b/lib/plugins/multipart_parser.js @@ -36,6 +36,8 @@ function multipartBodyParser(options) { return next(); var form = new formidable.IncomingForm(); + form.keepExtensions = options.keepExtensions ? true : false; + if (options.uploadDir) form.uploadDir = options.uploadDir; return form.parse(req, function (err, fields, files) { if (err) @@ -53,9 +55,16 @@ function multipartBodyParser(options) { req.params[k] = fields[k]; }); + + if (req.params.files && !options.overrideParams) { + req.log.warn('parameter files was already sent'); + return; + } + req.params.files = files; } req.log.trace('(multipart): fields=%j', fields); + req.log.trace('(multipart): files=%j', files); return next(); }); }; diff --git a/lib/request.js b/lib/request.js index 1ea904479..c66e5b09d 100644 --- a/lib/request.js +++ b/lib/request.js @@ -9,6 +9,7 @@ var mime = require('mime'); var qs = require('qs'); var uuid = require('node-uuid'); +var sanitizePath = require('./utils').sanitizePath; ///--- Globals @@ -19,26 +20,6 @@ var Request = http.IncomingMessage; ///--- 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); - - // Be nice like apache and strip out any //my//foo//bar///blah - path = path.replace(/\/\/+/g, '/'); - - // Kill a trailing '/' - if (path.lastIndexOf('/') === (path.length - 1) && path.length > 1) - path = path.substr(0, path.length - 1); - - return path; -} - - // The following three functions are courtesy of expressjs // as is req.accepts(), and req.is() below. // @@ -98,16 +79,22 @@ function parseKeepAlive(req) { return req.httpVersion === '1.0' ? false : true; } +function parseContentLength(req) { + var length = req.headers['content-length']; + return (length === undefined) ? null : parseInt(length, 10); +} ///--- API module.exports = { - extendRequest: function extendRequest(req) { - var _url = url.parse(req.url); + extendRequest: function extendRequest(options) { + var req = options.request, + _url = url.parse(req.url); + req.log = options.log; req.chunked = req.headers['transfer-encoding'] === 'chunked'; - req.contentLength = req.headers['content-length'] || 0; + req.contentLength = parseContentLength(req); req.contentType = parseContentType(req); req.href = _url.href; req.id = req.headers['x-request-id'] || uuid(); diff --git a/lib/response.js b/lib/response.js index 3ed8b7e46..8311635ab 100644 --- a/lib/response.js +++ b/lib/response.js @@ -344,13 +344,14 @@ Response.prototype.defaultResponseHeaders = function defaultHeaders(data) { this.setHeader('Content-MD5', hash.digest('base64')); } - if (!this.header('Content-Type') && this.contentType) { - var type = this.contentType; + if (this.header('Content-Type') || this.contentType) { + var type = this.header('Content-Type') || this.contentType; if (this.charSet) type += '; charset=' + this.charSet; this.setHeader('Content-Type', type); } + var now = new Date(); if (!this.getHeader('Date')) this.setHeader('Date', httpDate(now)); @@ -398,7 +399,7 @@ Response.prototype.send = function send(body) { // serialization var data = body ? this.format(body) : null; this.emit('beforeSend', data); - + this.defaultResponseHeaders(data); this.writeHead(this.statusCode, this.headers); if (data && !head && this.statusCode !== 204 && this.statusCode !== 304) { diff --git a/lib/route.js b/lib/route.js index 115f34f20..3e0acc4cd 100644 --- a/lib/route.js +++ b/lib/route.js @@ -176,7 +176,7 @@ function Route(options) { self._url = url.parse(u).pathname; self.pattern = '^'; - self.flags = ''; + self.flags = options.flags || ''; self.params = []; self._url.split('/').forEach(function (fragment) { if (!fragment.length) @@ -184,12 +184,17 @@ function Route(options) { self.pattern += '\\/+'; if (fragment.charAt(0) === ':') { - // Previously was gratuitous, but better to just be standard - // self.pattern += '([a-zA-Z0-9-_~%!;@=+\\$\\*\\.]+)'; - // - // See RFC3986, or this handy table: - // http://en.wikipedia.org/wiki/Percent-encoding#Types_of_URI_characters - self.pattern += '([a-zA-Z0-9-_~\\.%]+)'; + if (options.urlParamPattern) { + self.pattern += '(' + options.urlParamPattern + ')'; + } + else { + // Previously was gratuitous, but better to just be standard + // self.pattern += '([a-zA-Z0-9-_~%!;@=+\\$\\*\\.]+)'; + // + // See RFC3986, or this handy table: + // http://en.wikipedia.org/wiki/Percent-encoding#Types_of_URI_characters + self.pattern += '([a-zA-Z0-9-_~\\.%]+)'; + } self.params.push(fragment.slice(1)); } else { self.pattern += fragment; diff --git a/lib/server.js b/lib/server.js index 0002b25ba..ac3790f5d 100644 --- a/lib/server.js +++ b/lib/server.js @@ -41,7 +41,8 @@ function argsToChain() { handlers.forEach(function (h) { if (Array.isArray(h)) return process(h); - if (!typeof (h) === 'function') + + if (typeof (h) !== 'function') throw new TypeError('handlers must be Functions'); return chain.push(h); @@ -117,6 +118,7 @@ function Server(options) { this.name = options.name || 'restify'; this.preChain = []; this.routes = []; + this.urlParamPattern = options.urlParamPattern; this.version = options.version || false; this.responseTimeHeader = options.responseTimeHeader || 'X-Response-Time'; this.responseTimeFormatter = options.responseTimeFormatter; @@ -135,10 +137,11 @@ function Server(options) { if (options.certificate && options.key) { secure = true; - this.server = https.createServer({ - cert: options.certificate, - key: options.key - }); + var httpsOptions = { cert: options.certificate, key: options.key }; + if (options.ca) + httpsOptions.ca = options.ca; + + this.server = https.createServer(httpsOptions); } else { this.server = http.createServer(); } @@ -417,10 +420,12 @@ Server.prototype._addRoute = function _addRoute(method, options, handlers) { log: self.log, method: method, url: options.path || options.url, + flags: options.flags, handlers: chain, name: options.name, version: options.version || self.version, - dtrace: self.dtrace + dtrace: self.dtrace, + urlParamPattern: self.urlParamPattern }); route.on('error', function (err) { self.emit('error', err); @@ -455,7 +460,10 @@ Server.prototype._addRoute = function _addRoute(method, options, handlers) { Server.prototype._request = function _request(request, response, expect100) { var self = this; - var req = extendRequest(request); + var req = extendRequest({ + log: self.log, + request: request + }); var res = extendResponse({ formatters: self.formatters, log: self.log, @@ -584,10 +592,11 @@ Server.prototype._findRoute = function _findRoute(req, res) { * @param {Function} The middleware function to execute */ Server.prototype.param = function param(name, fn) { - return this.use(function (req, res, next) { - if (req.params && req.params[name]) - return fn.apply(this, arguments); + return this.use(function (req, res, next) { + var value = req.params && req.params[name]; + if (value) + return fn.call(this, req, res, next, value, name); - return next(); - }); + return next(); + }); }; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 000000000..935891197 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,22 @@ +// Copyright 2012 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); + +/** + * 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. + */ +exports.sanitizePath = function sanitizePath(path) { + assert.ok(path); + + // Be nice like apache and strip out any //my//foo//bar///blah + path = path.replace(/\/\/+/g, '/'); + + // Kill a trailing '/' + if (path.lastIndexOf('/') === (path.length - 1) && path.length > 1) + path = path.substr(0, path.length - 1); + + return path; +}; diff --git a/package.json b/package.json index 7d16a0fe5..0321b1844 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { "author": "Mark Cavage ", "contributors": [ + "Luri Aranda", "Dominic Barnes", + "Tamas Daniel", "Domenic Denicola", "Paul Bouzakis", "Shaun Berryman", @@ -10,49 +12,51 @@ "Trent Mick", "Falco Nogatz", "Pedro Palazón", + "Petter Rasmussen", "Andrew Robinson", "Isaac Schlueter", "Andrew Sliwinski", "Matt Smillie", + "Simon Sturmer", "Diego Torres", + "Jonathan Wiepert", "Mike Williams" ], "name": "restify", "homepage": "http://mcavage.github.com/node-restify", "description": "REST framework", - "version": "1.4.1", + "version": "1.4.4", "repository": { "type": "git", "url": "git://github.com/mcavage/node-restify.git" }, "main": "lib/index.js", - "directories": { - "bin": "./bin", - "lib": "./lib" + "bin": { + "report-latency": "./bin/report-latency" }, "engines": { "node": ">=0.6" }, "dependencies": { - "async": "0.1.18", - "bunyan": "0.6.8", + "async": "0.1.22", + "bunyan": "0.10.0", "byline": "2.0.2", - "formidable": "1.0.9", - "dtrace-provider": "0.0.6", + "formidable": "1.0.11", + "dtrace-provider": "0.0.9", "http-signature": "0.9.9", - "lru-cache": "1.0.5", + "lru-cache": "1.1.0", "mime": "1.2.5", "node-uuid": "1.3.3", - "qs": "0.4.2", + "qs": "0.5.0", "retry": "0.6.0", - "semver": "1.0.13" + "semver": "1.0.14" }, "devDependencies": { "filed": "0.0.6", - "tap": "0.2.4" + "tap": "0.2.5" }, "optionalDependencies": { - "dtrace-provider": "0.0.6" + "dtrace-provider": "0.0.9" }, "scripts": { "test": "./node_modules/.bin/tap ./test/*.test.js" diff --git a/test/client.test.js b/test/client.test.js index 9daf1dc95..c47fd819a 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -39,7 +39,20 @@ function sendText(req, res, next) { return next(); } +function sendWhitespace(req, res, next) { + var body = ' '; + if (req.params.flavor === 'spaces') { + body = ' '; + } else if (req.params.flavor === 'tabs') { + body = ' \t\t '; + } + + // override contentType as otherwise the string is json-ified to include quotes. Don't want that for this test. + res.contentType = 'text/plain'; + res.send(body); + return next(); +} ///--- Tests @@ -70,6 +83,8 @@ test('setup', function (t) { server.put('/str/:name', sendText); server.post('/str/:name', sendText); + server.get('/whitespace/:flavor', sendWhitespace); + server.listen(PORT, '127.0.0.1', function () { t.end(); }); @@ -99,6 +114,25 @@ test('GET json', function (t) { }); }); +test('GH-203 GET json, body is whitespace', function (t) { + client.get('/whitespace/spaces', function (err, req, res, obj) { + t.ifError(err); + t.ok(req); + t.ok(res); + t.equivalent(obj, {}); + t.end(); + }); +}); + +test('GH-203 GET json, body is tabs', function (t) { + client.get('/whitespace/tabs', function (err, req, res, obj) { + t.ifError(err); + t.ok(req); + t.ok(res); + t.equivalent(obj, {}); + t.end(); + }); +}); test('GH-115 GET path with spaces', function (t) { client.get('/json/foo bar', function (err, req, res, obj) { @@ -144,6 +178,13 @@ test('POST json', function (t) { }); }); +test('POST nothing', function (t) { + t.throws(function () { + client.post('/json/mcavage', function(){}); + }, new TypeError('body (Object) required')); + t.end(); +}); + test('PUT json', function (t) { client.post('/json/mcavage', { hello: 'foo' }, function (err, req, res, obj) { @@ -156,6 +197,93 @@ test('PUT json', function (t) { }); +test('GH-169 PUT json Content-MD5', function (t) { + var msg = { + "_id": "4ff71172bc148900000010a3", + "userId": "4f711b377579dbf65e000001", + "courseId": "4f69021bff338faffa000001", + "createdByUserId": "4f711b377579dbf65e000001", + "dateFrom": "2012-06-04", + "dateTo": "2012-09-30", + "notes": "Rates do not include tax & are subject to change without notice\\nRental Clubs are available for $30 per set\\nAll major credit cards accepted", + "updatedAt": "2012-07-06T17:59:08.581Z", + "periods": [{ + "name": "morning", + "weekdayWalking": 1500, + "weekdayCart": 3000, + "weekendWalking": 2000, + "weekendCart": 3500, + "timeFrom": 0, + "timeTo": 780, + "_id": "4ff71172bc148900000010a4" + }, + { + "timeFrom": 780, + "name": "twilight", + "timeTo": 900, + "weekdayWalking": 1500, + "weekdayCart": 2500, + "weekendWalking": 1500, + "weekendCart": 3000, + "_id": "4ff7276cbc148900000010f4" + }, + { + "timeFrom": 900, + "name": "super twilight", + "weekdayWalking": 1200, + "weekdayCart": 2000, + "weekendWalking": 1200, + "weekendCart": 2500, + "timeTo": 1439, + "_id": "4ff7276cbc148900000010f3" + }], + "holidays": [{ + "country": "US", + "name": "Flag Day", + "start": 1339657200000, + "end": 1339743600000, + "date": "2012-06-14" + }, + { + "country": "US / MX", + "name": "Father's Day, Día del Padre (Father's Day)", + "start": 1340262000000, + "end": 1340348400000, + "date": "2012-06-21" + }, + { + "country": "US", + "name": "Independence Day", + "start": 1341385200000, + "end": 1341471600000, + "date": "2012-07-04" + }, + { + "country": "US", + "name": "Labor Day", + "start": 1347001200000, + "end": 1347087600000, + "date": "2012-09-07" + }], + "weekdaySunday": false, + "weekdaySaturday": false, + "weekdayFriday": false, + "weekdayThursday": true, + "weekdayWednesday": true, + "weekdayTuesday": true, + "weekdayMonday": true + }; + + client.put('/json/md5', msg, function (err, req, res, obj) { + t.ifError(err); + t.ok(req); + t.ok(res); + t.equivalent(obj, {hello: 'md5'}); + t.end(); + }); +}); + + test('create string client', function (t) { client = restify.createClient({ url: 'http://127.0.0.1:' + PORT, diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 000000000..1dc925ee5 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,28 @@ +// Copyright 2012 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var restify = require('../lib/index'); + + + +///--- Globals + +var realizeUrl = restify.realizeUrl; + + +///--- Tests + +test('realize', function (t) { + var pattern = '/foo/:bar/:baz'; + + t.equal(realizeUrl(pattern, {}), '/foo/:bar/:baz'); + t.equal(realizeUrl(pattern, {bar: 'BAR'}), '/foo/BAR/:baz'); + t.equal(realizeUrl(pattern, {bar: 'BAR', baz: 'BAZ'}), '/foo/BAR/BAZ'); + t.equal(realizeUrl(pattern, {bar: 'BAR', baz: 'BAZ', quux: 'QUUX'}), + '/foo/BAR/BAZ'); + + t.equal(realizeUrl('/foo////bar///:baz', {baz: 'BAZ'}), '/foo/bar/BAZ'); + + t.end(); +}); diff --git a/test/route.test.js b/test/route.test.js index e8fcfac06..f7a9542eb 100644 --- a/test/route.test.js +++ b/test/route.test.js @@ -218,6 +218,34 @@ test('test matches params url-encoded', function (t) { }); +test('test matches params with custom regex', function (t) { + var route = new Route({ + log: new Logger({name: 'restify/test/route'}), + url: '/foo/:bar', + method: 'GET', + urlParamPattern: '[a-zA-Z0-9-_~%!;@=+\\$\\*\\.]+' + }); + t.ok(route); + t.equivalent(route.matches({ + method: 'GET', + path: '/foo/a%40b.com' + }), { bar: 'a@b.com' }); + t.ok(route.matches({ + method: 'GET', + path: '/foo/a@b.com' + }), { bar: 'a@b.com' }); + t.ok(route.matches({ + method: 'GET', + path: '/foo/a*b.com' + }), { bar: 'a*b.com' }); + t.notOk(route.matches({ + method: 'GET', + path: '/foo/a%40b.com/bar' + })); + t.end(); +}); + + test('test matches multiple params', function (t) { var route = new Route({ log: new Logger({name: 'restify/test/route'}), diff --git a/test/server.test.js b/test/server.test.js index 6f13b7f90..8b678e8b7 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -250,6 +250,31 @@ test('get (path and version not ok)', function (t) { }); }); +test('use - throws TypeError on non function as argument', function (t) { + + var server = restify.createServer({ dtrace: DTRACE, log: LOGGER }); + + var err = new TypeError('handlers must be Functions'); + + t.throws(function () { + server.use('/nonfn'); + }, err); + + t.throws(function () { + server.use({an:'object'}); + }, err); + + t.throws(function () { + server.use( + function good() { return next() }, + '/bad', + {really:'bad'} + ); + }, err); + + + t.end(); +}); test('use + get (path only)', function (t) { var server = restify.createServer({ dtrace: DTRACE, log: LOGGER }); @@ -527,6 +552,32 @@ test('RegExp ok', function (t) { }); +test('path+flags ok', function (t) { + var server = restify.createServer({ dtrace: DTRACE, log: LOGGER }); + + server.get({path: '/foo', flags: 'i'}, function tester(req, res, next) { + res.send('hi there'); + return next(); + }); + + server.listen(PORT, function () { + var opts = { + hostname: 'localhost', + port: PORT, + path: '/FOO', + method: 'GET', + agent: false + }; + http.request(opts, function (res) { + t.equal(res.statusCode, 200); + server.close(function () { + t.end(); + }); + }).end(); + }); +}); + + test('GH-56 streaming with filed (download)', function (t) { var server = restify.createServer({ dtrace: DTRACE, log: LOGGER }); @@ -641,6 +692,7 @@ test('GH-64 prerouting chain', function (t) { var server = restify.createServer({ dtrace: DTRACE, log: LOGGER }); server.pre(function (req, res, next) { + req.log.info('prerouting chain'); req.headers.accept = 'application/json'; return next(); }); @@ -949,6 +1001,53 @@ test('GH-109 RegExp flags not honored', function (t) { }); +test('GH-141 return next(err) not working', function (t) { + var server = restify.createServer({ dtrace: DTRACE, log: LOGGER }); + server.use(restify.authorizationParser()); + server.use(function authenticate(req, res, next) { + if (req.username !== 'admin' || + !req.authorization.basic || + req.authorization.basic.password !== 'admin') { + return next(new restify.NotAuthorizedError('invalid credentials')); + } + return next(); + }); + + server.get('/', function (req, res, next) { + res.send(200, req.username); + return next(); + }); + + server.listen(PORT, function () { + var opts = { + hostname: 'localhost', + port: PORT, + path: '/', + method: 'GET', + agent: false, + headers: { + accept: 'text/plain', + authorization: 'Basic ' + new Buffer('admin:foo').toString('base64') + } + }; + http.request(opts, function (res) { + t.equal(res.statusCode, 403); + var body = ''; + res.setEncoding('utf8'); + res.on('data', function (chunk) { + body += chunk; + }); + res.on('end', function () { + t.equal(body, 'invalid credentials'); + server.close(function () { + t.end(); + }); + }); + }).end(); + }); +}); + + // // Disabled, as Heroku (travis) doesn't allow us to write to /tmp // @@ -989,3 +1088,75 @@ test('GH-109 RegExp flags not honored', function (t) { // }); // + + +test('GH-149 limit request body size (form)', function (t) { + var server = restify.createServer(); + server.use(restify.bodyParser({maxBodySize: 1024})); + + server.post('/', function (req, res, next) { + res.send(200, {length: req.body.length}); + return next(); + }); + + server.listen(PORT, function () { + var opts = { + hostname: 'localhost', + port: PORT, + path: '/', + method: 'POST', + agent: false, + headers: { + 'accept': 'application/json', + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked' + } + }; + var req = http.request(opts, function (res) { + t.equal(res.statusCode, 413); + res.on('end', function () { + server.close(function () { + t.end(); + }); + }); + }); + req.write(new Array(1026).join('x')); + req.end(); + }); +}); + + +test('GH-149 limit request body size (json)', function (t) { + var server = restify.createServer(); + server.use(restify.bodyParser({maxBodySize: 1024})); + + server.post('/', function (req, res, next) { + res.send(200, {length: req.body.length}); + return next(); + }); + + server.listen(PORT, function () { + var opts = { + hostname: 'localhost', + port: PORT, + path: '/', + method: 'POST', + agent: false, + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + 'transfer-encoding': 'chunked' + } + }; + var req = http.request(opts, function (res) { + t.equal(res.statusCode, 413); + res.on('end', function () { + server.close(function () { + t.end(); + }); + }); + }); + req.write('{"a":[' + new Array(512).join('1,') + '0]}'); + req.end(); + }); +});