2021-12-03 17:58:48 +01:00

198 lines
5.3 KiB
JavaScript

// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var crypto = require('crypto');
var zlib = require('zlib');
var assert = require('assert-plus');
var once = require('once');
var errors = require('restify-errors');
///--- Globals
var BadDigestError = errors.BadDigestError;
var RequestEntityTooLargeError = errors.RequestEntityTooLargeError;
var PayloadTooLargeError = errors.PayloadTooLargeError;
var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;
var MD5_MSG = "Content-MD5 '%s' didn't match '%s'";
///--- Helpers
function createBodyWriter(req) {
var buffers = [];
var contentType = req.contentType();
var isText = false;
if (
!contentType ||
contentType === 'application/json' ||
contentType === 'application/x-www-form-urlencoded' ||
contentType === 'multipart/form-data' ||
contentType.substr(0, 5) === 'text/'
) {
isText = true;
}
req.body = new Buffer(0);
return {
write: function write(chunk) {
buffers.push(chunk);
},
end: function end() {
req.body = Buffer.concat(buffers);
if (isText) {
req.body = req.body.toString('utf8');
}
}
};
}
///--- API
/**
* Reads the body of the request.
*
* @public
* @function bodyReader
* @throws {BadDigestError | PayloadTooLargeError}
* @param {Object} options - an options object
* @returns {Function} Handler
*/
function bodyReader(options) {
var opts = options || {};
assert.object(opts, 'opts');
var maxBodySize = opts.maxBodySize || 0;
function readBody(req, res, originalNext) {
var next = once(originalNext);
// #100 don't read the body again if we've read it once
if (req._readBody) {
next();
return;
} else {
req._readBody = true;
}
if (
(req.getContentLength() === 0 && !req.isChunked()) ||
req.contentType() === 'multipart/form-data' ||
req.contentType() === 'application/octet-stream'
) {
next();
return;
}
var bodyWriter = createBodyWriter(req);
var bytesReceived = 0;
var digest;
var gz;
var hash;
var md5;
var unsupportedContentEncoding;
if ((md5 = req.headers['content-md5'])) {
hash = crypto.createHash('md5');
}
function done() {
bodyWriter.end();
if (unsupportedContentEncoding) {
next(
new UnsupportedMediaTypeError(
{
info: {
contentEncoding: unsupportedContentEncoding
}
},
'content encoding not supported'
)
);
return;
}
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);
return;
}
if (!req.body.length) {
next();
return;
}
if (hash && md5 !== (digest = hash.digest('base64'))) {
next(new BadDigestError(MD5_MSG, md5, digest));
return;
}
next();
}
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);
gz.once('end', done);
req.once('end', gz.end.bind(gz));
} else {
unsupportedContentEncoding = req.headers['content-encoding'];
res.setHeader('Accept-Encoding', 'gzip');
req.once('end', done);
}
req.on('data', function onRequestData(chunk) {
if (maxBodySize) {
bytesReceived += chunk.length;
if (bytesReceived > maxBodySize) {
return;
}
}
if (hash) {
hash.update(chunk, 'binary');
}
if (gz) {
gz.write(chunk);
} else {
bodyWriter.write(chunk);
}
});
req.once('error', next);
// add 'close and 'aborted' event handlers so that requests (and their
// corresponding memory) don't leak if client stops sending data half
// way through a POST request
req.once('close', next);
req.once('aborted', next);
req.resume();
}
return readBody;
}
module.exports = bodyReader;