From 7147b4a849785fcc67c4a047b581e1b546b2531b Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Thu, 4 Apr 2013 13:03:17 -0700 Subject: [PATCH] request.rawBody is now a buffer --- lib/pack.js | 4 +- lib/payload.js | 67 +++++++------- lib/response/generic.js | 1 + lib/response/raw.js | 1 + test/integration/auth.js | 2 +- test/integration/payload.js | 176 +++++++++++++----------------------- 6 files changed, 100 insertions(+), 151 deletions(-) diff --git a/lib/pack.js b/lib/pack.js index 15b028d5d..845e3bcd5 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -408,9 +408,7 @@ internals.getSourceFilePath = function () { for (var i = 0, il = stack.length; i < il; ++i) { var stackLine = stack[i]; - if (stackLine[3] === 'internals.Pack.require' || - stackLine[3] === 'internals.Pack.allow.scoped.require') { // The file that calls require is next - + if (stackLine[3].lastIndexOf('.require') === stackLine[3].length - 8) { // The file that calls require is next callerFile = stack[i + 1][0]; break; } diff --git a/lib/payload.js b/lib/payload.js index d23e62cf9..1cb56ad75 100755 --- a/lib/payload.js +++ b/lib/payload.js @@ -66,10 +66,6 @@ exports.read = function (request, next) { // Read incoming body - var isGZip = (req.headers['content-encoding'] === 'gzip'); - var encoding = isGZip ? 'base64' : 'utf8'; - req.setEncoding(encoding); - req.on('close', function () { return finish(Boom.internal('Request closed before finished reading')); @@ -80,7 +76,9 @@ exports.read = function (request, next) { return finish(Boom.internal('Request error before finished reading: ' + err)); }); - var payload = ''; + var buffers = []; + var buffersLength = 0 + req.on('readable', function () { var chunk = req.read(); @@ -88,68 +86,71 @@ exports.read = function (request, next) { return; } - if (payload.length + chunk.length > request.server.settings.payload.maxBytes) { + if (buffersLength + chunk.length > request.server.settings.payload.maxBytes) { return finish(Boom.badRequest('Payload size greater than maximum allowed: ' + request.server.settings.payload.maxBytes)); } - payload += chunk; + buffers.push(chunk); + buffersLength += chunk.length; }); req.on('end', function () { - if (isBailed) { - return; // next() already called - } - - var unzip = function () { + var review = function () { - var payloadBuf = new Buffer(payload, encoding); - if (!internals.isDeflate(payloadBuf) && !internals.isGzip(payloadBuf)) { - return finish(Boom.badRequest('Invalid compressed payload')); + if (isBailed) { + return; // next() already called } - Zlib.unzip(payloadBuf, function (err, buffer) { // Err shouldn't exist since the buffer is validated above + var payload = Buffer.concat(buffers, buffersLength); - var unzipped = buffer.toString(); - request.rawBody = unzipped; - return parse(unzipped); - }); + if (payload.length && + req.headers['content-encoding'] === 'gzip') { + + if (!internals.isDeflate(payload) && !internals.isGzip(payload)) { + return finish(Boom.badRequest('Invalid compressed payload')); + } + + Zlib.unzip(payload, function (err, unzipped) { // Err shouldn't exist since the buffer is validated above + + return parse(unzipped); + }); + + return; + } + + parse(payload); }; - var parse = function (result) { + var parse = function (payload) { - if (level !== 'parse') { // 'raw' + request.rawBody = (payload.length ? payload : null); + + if (level !== 'parse') { // 'raw' return finish(); } request.payload = {}; - if (!result) { + if (!payload.length) { return finish(); } // Set parser - internals.parse(result, req.headers, function (err, payload) { + internals.parse(payload.toString('utf8'), req.headers, function (err, result) { if (err) { return finish(err); } - request.payload = payload; + request.payload = result; return finish(); }); }; - if (isGZip) { - return unzip(); - } - - request.rawBody = payload; - parse(payload); + review(); }); - - req.read(0); }; diff --git a/lib/response/generic.js b/lib/response/generic.js index a293ef54f..f1ee40dfa 100755 --- a/lib/response/generic.js +++ b/lib/response/generic.js @@ -166,6 +166,7 @@ internals.Generic.prototype._transmit = function (request, callback) { }); } + self.peek.emit('end'); request.raw.res.end(); callback(); diff --git a/lib/response/raw.js b/lib/response/raw.js index 784cde5ed..6713a6162 100755 --- a/lib/response/raw.js +++ b/lib/response/raw.js @@ -78,6 +78,7 @@ internals.Raw.prototype._transmit = function (request, callback) { this.begin(function (err) { // Flush header if begin() not called. Too late to handle the error (ignored). delete self.write; + self.peek.emit('end'); self._request.raw.res.end(); return callback(); }); diff --git a/test/integration/auth.js b/test/integration/auth.js index f1f165a37..ba1825fc1 100755 --- a/test/integration/auth.js +++ b/test/integration/auth.js @@ -652,7 +652,7 @@ describe('Auth', function () { setTimeout(function () { - this.emit('end'); + this.push(null); }, 5); }; diff --git a/test/integration/payload.js b/test/integration/payload.js index 9cd52d482..79df36d18 100755 --- a/test/integration/payload.js +++ b/test/integration/payload.js @@ -66,42 +66,42 @@ describe('Payload', function () { it('returns a raw body', function (done) { - var multipartPayload = '{"x":"1","y":"2","z":"3"}'; + var payload = '{"x":"1","y":"2","z":"3"}'; var handler = function (request) { expect(request.payload).to.not.exist; - expect(request.rawBody).to.equal(multipartPayload); + expect(request.rawBody.toString()).to.equal(payload); request.reply(request.rawBody); }; var server = new Hapi.Server(); server.route({ method: 'POST', path: '/', config: { handler: handler, payload: 'raw' } }); - server.inject({ method: 'POST', url: '/', payload: multipartPayload }, function (res) { + server.inject({ method: 'POST', url: '/', payload: payload }, function (res) { expect(res.result).to.exist; - expect(res.result).to.equal(multipartPayload); + expect(res.result).to.equal(payload); done(); }); }); it('returns a parsed body and sets a raw body', function (done) { - var multipartPayload = '{"x":"1","y":"2","z":"3"}'; + var payload = '{"x":"1","y":"2","z":"3"}'; var handler = function (request) { expect(request.payload).to.exist; expect(request.payload.z).to.equal('3'); - expect(request.rawBody).to.equal(multipartPayload); + expect(request.rawBody.toString()).to.equal(payload); request.reply(request.payload); }; var server = new Hapi.Server(); server.route({ method: 'POST', path: '/', config: { handler: handler } }); - server.inject({ method: 'POST', url: '/', payload: multipartPayload }, function (res) { + server.inject({ method: 'POST', url: '/', payload: payload }, function (res) { expect(res.result).to.exist; expect(res.result.x).to.equal('1'); @@ -120,13 +120,12 @@ describe('Payload', function () { var server = new Hapi.Server('0.0.0.0', 0); server.route({ method: 'POST', path: '/', config: { handler: handler, payload: 'raw' } }); - var s = new Stream(); - s.readable = true; + var s = new Stream.PassThrough(); server.start(function () { var options = { - hostname: '127.0.0.1', + hostname: 'localhost', port: server.settings.port, path: '/', method: 'POST' @@ -134,7 +133,7 @@ describe('Payload', function () { var iv = setInterval(function () { - s.emit('data', 'Hello'); + s.write('Hello'); }, 5); var req = Http.request(options, function (res) { @@ -171,7 +170,7 @@ describe('Payload', function () { server.start(function () { var options = { - hostname: '127.0.0.1', + hostname: 'localhost', port: server.settings.port, path: '/', method: 'POST', @@ -201,44 +200,44 @@ describe('Payload', function () { it('returns the correct raw body when content-length is smaller than payload', function (done) { - var multipartPayload = '{"x":"1","y":"2","z":"3"}'; + var payload = '{"x":"1","y":"2","z":"3"}'; var handler = function (request) { expect(request.payload).to.not.exist; - expect(request.rawBody).to.equal(multipartPayload); + expect(request.rawBody.toString()).to.equal(payload); request.reply(request.rawBody); }; var server = new Hapi.Server(); server.route({ method: 'POST', path: '/', config: { handler: handler, payload: 'raw' } }); - server.inject({ method: 'POST', url: '/', payload: multipartPayload, headers: { 'Content-Length': '5' } }, function (res) { + server.inject({ method: 'POST', url: '/', payload: payload, headers: { 'Content-Length': '5' } }, function (res) { expect(res.result).to.exist; - expect(res.result).to.equal(multipartPayload); + expect(res.result).to.equal(payload); done(); }); }); it('returns the correct raw body when content-length is larger than payload', function (done) { - var multipartPayload = '{"x":"1","y":"2","z":"3"}'; + var payload = '{"x":"1","y":"2","z":"3"}'; var handler = function (request) { expect(request.payload).to.not.exist; - expect(request.rawBody).to.equal(multipartPayload); + expect(request.rawBody.toString()).to.equal(payload); request.reply(request.rawBody); }; var server = new Hapi.Server(); server.route({ method: 'POST', path: '/', config: { handler: handler, payload: 'raw' } }); - server.inject({ method: 'POST', url: '/', payload: multipartPayload, headers: { 'Content-Length': '500' } }, function (res) { + server.inject({ method: 'POST', url: '/', payload: payload, headers: { 'Content-Length': '500' } }, function (res) { expect(res.result).to.exist; - expect(res.result).to.equal(multipartPayload); + expect(res.result).to.equal(payload); done(); }); }); @@ -252,7 +251,7 @@ describe('Payload', function () { request.reply('Success'); }; - var server = new Hapi.Server('127.0.0.1', 0); + var server = new Hapi.Server('localhost', 0); server.route({ method: 'POST', path: '/', config: { handler: handler, payload: 'stream' } }); before(function (done) { @@ -263,18 +262,17 @@ describe('Payload', function () { it('doesn\'t set the request payload when streaming data in and the connection is interrupted', function (done) { var options = { - hostname: '127.0.0.1', + hostname: 'localhost', port: server.settings.port, path: '/', method: 'POST' }; - var s = new Stream(); - s.readable = true; + var s = new Stream.PassThrough(); var iv = setInterval(function () { - s.emit('data', 'Hello'); + s.write('Hello'); }, 5); var req = Http.request(options, function (res) { @@ -301,11 +299,10 @@ describe('Payload', function () { var handler = function (request) { - expect(request.payload.key).to.equal('value'); - request.reply('Success'); + request.reply(request.payload.key); }; - var server = new Hapi.Server('127.0.0.1', 0, { timeout: { client: 50 } }); + var server = new Hapi.Server('localhost', 0, { timeout: { client: 50 } }); server.route({ method: 'POST', path: '/', config: { handler: handler, payload: 'parse' } }); before(function (done) { @@ -313,42 +310,46 @@ describe('Payload', function () { server.start(done); }); + var TestStream = function () { + + Stream.Readable.call(this); + }; + + Hapi.utils.inherits(TestStream, Stream.Readable); + + TestStream.prototype._read = function (size) { + + this.push('{ "key": "value" }'); + this.push(null); + }; + it('sets the request payload with the streaming data', function (done) { var options = { - hostname: '127.0.0.1', - port: server.settings.port, - path: '/', + uri: 'http://localhost:' + server.settings.port + '/?x=1', method: 'POST', headers: { 'Content-Type': 'application/json' } }; - var s = new Stream(); - s.readable = true; - - var req = Http.request(options, function (res) { + var req = Request(options, function (err, res, body) { expect(res.statusCode).to.equal(200); - res.on('data', function (data) { - - expect(data.toString()).to.equal('Success'); - done(); - }); + expect(body).to.equal('value'); + done(); }); + var s = new TestStream(); s.pipe(req); - s.emit('data', '{ "key": "value" }'); - req.end(); }); it('times out when the request content-length is larger than payload', function (done) { var options = { - hostname: '127.0.0.1', + hostname: 'localhost', port: server.settings.port, - path: '/', + path: '/?x=2', method: 'POST', headers: { 'Content-Type': 'application/json', @@ -365,12 +366,11 @@ describe('Payload', function () { req.end('{ "key": "value" }'); }); - it('returns a 400 status code when the request content-length is smaller than payload', function (done) { + it('resets connection when the request content-length is smaller than payload', function (done) { var options = { - hostname: '127.0.0.1', - port: server.settings.port, - path: '/', + uri: 'http://localhost:' + server.settings.port + '/?x=3', + body: '{ "key": "value" }', method: 'POST', headers: { 'Content-Type': 'application/json', @@ -378,21 +378,19 @@ describe('Payload', function () { } }; - var req = Http.request(options, function (res) { + Request(options, function (err, res, body) { - expect(res.statusCode).to.equal(400); + expect(err.message).to.equal('socket hang up'); done(); }); - - req.end('{ "key": "value" }'); }); it('returns an error on unsupported mime type', function (done) { var options = { - hostname: '127.0.0.1', + hostname: 'localhost', port: server.settings.port, - path: '/', + path: '/?x=4', method: 'POST', headers: { 'Content-Type': 'application/unknown', @@ -412,9 +410,9 @@ describe('Payload', function () { it('returns 200 on text mime type', function (done) { var options = { - hostname: '127.0.0.1', + hostname: 'localhost', port: server.settings.port, - path: '/', + path: '/?x=5', method: 'POST', headers: { 'Content-Type': 'text/plain', @@ -430,73 +428,23 @@ describe('Payload', function () { req.end('{ "key": "value" }'); }); - - it('returns a 400 status code when the request payload is streaming data with content-length being too small', function (done) { - - var s = new Stream(); - s.readable = true; - - var options = { - uri: 'http://127.0.0.1:' + server.settings.port + '/', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': '1' - } - }; - - var req = Request(options, function (err, res) { - - expect(res.statusCode).to.equal(400); - done(); - }); - - s.pipe(req); - s.emit('data', '{ "key": "value" }'); - req.end(); - }); - - it('returns a 200 status code when the request payload is streaming data with content-length being smaller than payload but payload truncates to a valid value ', function (done) { - - var s = new Stream(); - s.readable = true; - - var options = { - uri: 'http://127.0.0.1:' + server.settings.port + '/', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': '18' - } - }; - - var req = Request(options, function (err, res) { - - expect(res.statusCode).to.equal(200); - done(); - }); - - s.pipe(req); - s.emit('data', '{ "key": "value" } garbage here'); - req.end(); - }); }); describe('unzip', function () { it('returns an error on malformed payload', function (done) { - var multipartPayload = '7d8d78347h8347d58w347hd58w374d58w37h5d8w37hd4'; + var payload = '7d8d78347h8347d58w347hd58w374d58w37h5d8w37hd4'; - var handler = function (request) { + var handler = function () { - expect(request).to.not.exist; // Must not be called + throw new Error('never called'); }; var server = new Hapi.Server(); server.route({ method: 'POST', path: '/', config: { handler: handler } }); - server.inject({ method: 'POST', url: '/', payload: multipartPayload, headers: { 'content-encoding': 'gzip' } }, function (res) { + server.inject({ method: 'POST', url: '/', payload: payload, headers: { 'content-encoding': 'gzip' } }, function (res) { expect(res.result).to.exist; expect(res.result.code).to.equal(400); @@ -506,13 +454,13 @@ describe('Payload', function () { it('doesn\'t return an error when the payload has the correct gzip header and gzipped payload', function (done) { - var multipartPayload = '{"hi":"hello"}'; + var payload = '{"hi":"hello"}'; - Zlib.gzip(multipartPayload, function (err, result) { + Zlib.gzip(payload, function (err, result) { - var handler = function (request) { + var handler = function () { - request.reply('Success'); + this.reply('Success'); }; var server = new Hapi.Server();