Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 49 additions & 18 deletions lib/plugins/bodyReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@ var errors = require('restify-errors');
///--- Globals

var BadDigestError = errors.BadDigestError;
var RequestEntityTooLargeError = errors.RequestEntityTooLargeError;
var InvalidContentError = errors.InvalidContentError;
var PayloadTooLargeError = errors.PayloadTooLargeError;
var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;

var MD5_MSG = "Content-MD5 '%s' didn't match '%s'";

///--- Helpers

function createBodyWriter(req) {
function createPayloadTooLargeError(maxBodySize) {
var msg = 'Request body size exceeds ' + maxBodySize;
return new PayloadTooLargeError(msg);
}

function createBodyWriter(req, maxBodySize) {
var buffers = [];
var decodedBytes = 0;

var contentType = req.contentType();
var isText = false;
Expand All @@ -39,6 +45,12 @@ function createBodyWriter(req) {
req.body = Buffer.alloc(0);
return {
write: function write(chunk) {
decodedBytes += chunk.length;

if (maxBodySize && decodedBytes > maxBodySize) {
throw createPayloadTooLargeError(maxBodySize);
}

buffers.push(chunk);
},
end: function end() {
Expand Down Expand Up @@ -87,13 +99,15 @@ function bodyReader(options) {
next();
return;
}
var bodyWriter = createBodyWriter(req);
var bodyWriter = createBodyWriter(req, maxBodySize);

var bodyReadFailed = false;
var bytesReceived = 0;
var digest;
var gz;
var hash;
var md5;
var onRequestEnd;

var unsupportedContentEncoding;

Expand All @@ -119,19 +133,7 @@ function bodyReader(options) {
}

if (maxBodySize && bytesReceived > maxBodySize) {
var msg = 'Request body size exceeds ' + maxBodySize;
var err;

// Between Node 0.12 and 4 http status code messages changed
// RequestEntityTooLarge was changed to PayloadTooLarge
// this check is to maintain backwards compatibility
if (PayloadTooLargeError !== undefined) {
err = new PayloadTooLargeError(msg);
} else {
err = new RequestEntityTooLargeError(msg);
}

next(err);
next(createPayloadTooLargeError(maxBodySize));
return;
}

Expand All @@ -148,25 +150,54 @@ function bodyReader(options) {
next();
}

function failGzipRead(err) {
if (bodyReadFailed) {
return;
}

bodyReadFailed = true;
req.removeListener('end', onRequestEnd);
gz.removeListener('end', done);
gz.destroy();
next(err);
}

if (req.headers['content-encoding'] === undefined) {
// This handles the original else branch
req.once('end', done);
} else if (req.headers['content-encoding'] === 'gzip') {
gz = zlib.createGunzip();
gz.on('data', bodyWriter.write);
onRequestEnd = gz.end.bind(gz);
gz.once('error', function onGzipError(err) {
failGzipRead(new InvalidContentError(err, 'Invalid gzip body'));
});
gz.on('data', function onGzipData(chunk) {
try {
bodyWriter.write(chunk);
} catch (err) {
failGzipRead(err);
}
});
gz.once('end', done);
req.once('end', gz.end.bind(gz));
req.once('end', onRequestEnd);
} else {
unsupportedContentEncoding = req.headers['content-encoding'];
res.setHeader('Accept-Encoding', 'gzip');
req.once('end', done);
}

req.on('data', function onRequestData(chunk) {
if (bodyReadFailed) {
return;
}

if (maxBodySize) {
bytesReceived += chunk.length;

if (bytesReceived > maxBodySize) {
if (gz) {
failGzipRead(createPayloadTooLargeError(maxBodySize));
}
return;
}
}
Expand Down
116 changes: 116 additions & 0 deletions test/plugins/bodyReader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
/* eslint-disable func-names */

// core requires
var crypto = require('crypto');
var http = require('http');
var zlib = require('zlib');

// external requires
var assert = require('chai').assert;
Expand Down Expand Up @@ -65,6 +67,120 @@ describe('body reader', function() {
);
});

it('should limit the decompressed body size', function(done) {
SERVER.use(restify.plugins.bodyParser({ maxBodySize: 1024 }));

CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
retry: false,
gzip: {}
});

SERVER.post('/compressed', function(req, res, next) {
assert.fail('route should not be called');
res.send();
next();
});

CLIENT.post(
'/compressed',
{
value: new Array(2049).join('x')
},
function(err, _, res) {
assert.isOk(err, 'should fail');
assert.equal(res.statusCode, 413);
done();
}
);
});

it('should limit compressed gzip body size', function(done) {
SERVER.use(restify.plugins.bodyParser({ maxBodySize: 1024 }));

CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
retry: false
});

SERVER.post('/compressed', function(req, res, next) {
assert.fail('route should not be called');
res.send();
next();
});

var compressedBody = zlib.gzipSync(crypto.randomBytes(2048));
assert.isAbove(compressedBody.length, 1024);

var request = http.request(
{
hostname: '127.0.0.1',
port: PORT,
path: '/compressed',
method: 'POST',
headers: {
'content-encoding': 'gzip',
'content-length': compressedBody.length,
'content-type': 'text/plain'
}
},
function(response) {
assert.equal(response.statusCode, 413);
response.resume();
response.once('end', done);
}
);
request.once('error', done);
request.end(compressedBody);
});

it('should reject malformed gzip and remain healthy', function(done) {
SERVER.use(restify.plugins.bodyParser({ maxBodySize: 1024 }));

CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
retry: false
});

SERVER.post('/compressed', function(req, res, next) {
assert.fail('route should not be called');
res.send();
next();
});
SERVER.get('/health', function(req, res, next) {
res.send({ healthy: true });
next();
});

var malformedBody = Buffer.from('not a gzip body');
var request = http.request(
{
hostname: '127.0.0.1',
port: PORT,
path: '/compressed',
method: 'POST',
headers: {
'content-encoding': 'gzip',
'content-length': malformedBody.length,
'content-type': 'text/plain'
}
},
function(response) {
assert.equal(response.statusCode, 400);
response.resume();
response.once('end', function() {
CLIENT.get('/health', function(err, _, healthResponse) {
assert.ifError(err);
assert.equal(healthResponse.statusCode, 200);
done();
});
});
}
);
request.once('error', done);
request.end(malformedBody);
});

it('should not accept unsupported content encoding', function(done) {
SERVER.use(restify.plugins.bodyParser());

Expand Down
Loading