diff --git a/lib/payload.js b/lib/payload.js index bef5222e1..543c1e03a 100755 --- a/lib/payload.js +++ b/lib/payload.js @@ -1,8 +1,9 @@ // Load modules +var Stream = require('stream'); var Zlib = require('zlib'); var Querystring = require('querystring'); -var Formidable = require('formidable'); +var Multiparty = require('multiparty'); var Boom = require('boom'); var Utils = require('./utils'); @@ -27,7 +28,7 @@ exports.read = function (request, next) { // Levels are: 'stream', 'raw', 'parse', 'try' - var level = request.route.payload.mode || (request.route.validate.payload || request.method === 'post' || request.method === 'put'|| request.method === 'patch' ? 'parse' : 'stream'); + var level = request.route.payload.mode || (request.route.validate.payload || request.method === 'post' || request.method === 'put' || request.method === 'patch' ? 'parse' : 'stream'); if (level === 'stream') { return next(); } @@ -223,38 +224,70 @@ internals.parse = function (payload, mime, headers, callback) { if (mime === 'multipart/form-data') { + var stream = new internals.Replay(headers, payload); + + var form = new Multiparty.Form(); var data = {}; - var form = new Formidable.IncomingForm(); - var processData = function (name, val) { + form.once('error', function (err) { + + return callback(Boom.badRequest('Invalid request multipart payload format')); + }); + + var set = function (name, value) { - if (data[name]) { - data[name] = [data[name], val]; + if (!data.hasOwnProperty(name)) { + data[name] = value; + } + else if (data[name] instanceof Array) { + data[name].push(value); } else { - data[name] = val; + data[name] = [data[name], value]; } }; - form.on('field', processData); - form.on('file', processData); + form.on('file', function (name, value) { - form.once('error', function () { + value.name = value.originalFilename; // For backwards compatibility with formidable + value.type = value.headers && value.headers['content-type']; + delete value.ws; + set(name, value); + }); - form.removeAllListeners(); - return callback(Boom.badRequest('Invalid request multipart payload format')); + form.on('field', function (name, value) { + + set(name, value); }); - form.once('end', function () { + form.once('close', function () { + stream.removeAllListeners(); form.removeAllListeners(); return callback(null, data); }); - form.writeHeaders(headers); - form.write(payload); + form.parse(stream); + return; } return callback(new Boom(415)); // Unsupported Media Type }; + + +internals.Replay = function (headers, payload) { + + Stream.Readable.call(this); + this.headers = headers; + this.__payload = payload; +}; + +Utils.inherits(internals.Replay, Stream.Readable); + + +internals.Replay.prototype._read = function (size) { + + this.push(this.__payload); + this.push(null); +}; diff --git a/package.json b/package.json index 3bcf85fb3..2c1c37f70 100755 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "router" ], "engines": { - "node": "0.10.x" + "node": ">=0.10.5" }, "dependencies": { "hoek": "0.8.x", @@ -38,7 +38,7 @@ "cryptiles": "0.2.x", "iron": "0.3.x", "async": "0.2.x", - "formidable": "1.0.13", + "multiparty": "2.1.x", "mime": "1.2.x", "lru-cache": "2.3.x", "optimist": "0.4.x", diff --git a/test/integration/payload.js b/test/integration/payload.js index 73fd1cc86..9f74cf710 100755 --- a/test/integration/payload.js +++ b/test/integration/payload.js @@ -554,11 +554,23 @@ describe('Payload', function () { request.reply(request.payload); }; - var _server = new Hapi.Server('0.0.0.0', 0); - _server.route({ method: 'POST', path: '/invalid', handler: invalidHandler }); - _server.route({ method: 'POST', path: '/echo', handler: echo }); + var server = new Hapi.Server('0.0.0.0', 0); + server.route({ method: 'POST', path: '/invalid', handler: invalidHandler }); + server.route({ method: 'POST', path: '/echo', handler: echo }); var multipartPayload = + '--AaB03x\r\n' + + 'content-disposition: form-data; name="x"\r\n' + + '\r\n' + + 'First\r\n' + + '--AaB03x\r\n' + + 'content-disposition: form-data; name="x"\r\n' + + '\r\n' + + 'Second\r\n' + + '--AaB03x\r\n' + + 'content-disposition: form-data; name="x"\r\n' + + '\r\n' + + 'Third\r\n' + '--AaB03x\r\n' + 'content-disposition: form-data; name="field1"\r\n' + '\r\n' + @@ -576,7 +588,7 @@ describe('Payload', function () { it('returns an error on missing boundary in content-type header', function (done) { - _server.inject({ method: 'POST', url: '/invalid', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data' } }, function (res) { + server.inject({ method: 'POST', url: '/invalid', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data' } }, function (res) { expect(res.result).to.exist; expect(res.result.code).to.equal(400); @@ -586,7 +598,7 @@ describe('Payload', function () { it('returns an error on empty separator in content-type header', function (done) { - _server.inject({ method: 'POST', url: '/invalid', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=' } }, function (res) { + server.inject({ method: 'POST', url: '/invalid', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=' } }, function (res) { expect(res.result).to.exist; expect(res.result.code).to.equal(400); @@ -596,9 +608,9 @@ describe('Payload', function () { it('returns parsed multipart data', function (done) { - _server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' } }, function (res) { + server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' } }, function (res) { - expect(Object.keys(res.result).length).to.equal(2); + expect(Object.keys(res.result).length).to.equal(3); expect(res.result.field1).to.exist; expect(res.result.field1.length).to.equal(2); expect(res.result.field1[1]).to.equal('Repeated name segment');