349 lines
7.2 KiB
JavaScript
349 lines
7.2 KiB
JavaScript
/*!
|
|
* raw-body
|
|
* Copyright(c) 2013-2014 Jonathan Ong
|
|
* Copyright(c) 2014-2022 Douglas Christopher Wilson
|
|
* MIT Licensed
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
* Module dependencies.
|
|
* @private
|
|
*/
|
|
|
|
var asyncHooks = tryRequireAsyncHooks();
|
|
var bytes = require('bytes');
|
|
var createError = require('http-errors');
|
|
var iconv = require('iconv-lite');
|
|
var unpipe = require('unpipe');
|
|
|
|
/**
|
|
* Module exports.
|
|
* @public
|
|
*/
|
|
|
|
module.exports = getRawBody;
|
|
|
|
/**
|
|
* Module variables.
|
|
* @private
|
|
*/
|
|
|
|
var ICONV_ENCODING_MESSAGE_REGEXP = /^Encoding not recognized: /;
|
|
|
|
/**
|
|
* Get the decoder for a given encoding.
|
|
*
|
|
* @param {string} encoding
|
|
* @private
|
|
*/
|
|
|
|
function getDecoder(encoding) {
|
|
if (!encoding) return null;
|
|
|
|
try {
|
|
return iconv.getDecoder(encoding);
|
|
} catch (e) {
|
|
// error getting decoder
|
|
if (!ICONV_ENCODING_MESSAGE_REGEXP.test(e.message)) throw e;
|
|
|
|
// the encoding was not found
|
|
throw createError(415, 'specified encoding unsupported', {
|
|
encoding: encoding,
|
|
type: 'encoding.unsupported',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the raw body of a stream (typically HTTP).
|
|
*
|
|
* @param {object} stream
|
|
* @param {object|string|function} [options]
|
|
* @param {function} [callback]
|
|
* @public
|
|
*/
|
|
|
|
function getRawBody(stream, options, callback) {
|
|
var done = callback;
|
|
var opts = options || {};
|
|
|
|
// light validation
|
|
if (stream === undefined) {
|
|
throw new TypeError('argument stream is required');
|
|
} else if (
|
|
typeof stream !== 'object' ||
|
|
stream === null ||
|
|
typeof stream.on !== 'function'
|
|
) {
|
|
throw new TypeError('argument stream must be a stream');
|
|
}
|
|
|
|
if (options === true || typeof options === 'string') {
|
|
// short cut for encoding
|
|
opts = {
|
|
encoding: options,
|
|
};
|
|
}
|
|
|
|
if (typeof options === 'function') {
|
|
done = options;
|
|
opts = {};
|
|
}
|
|
|
|
// validate callback is a function, if provided
|
|
if (done !== undefined && typeof done !== 'function') {
|
|
throw new TypeError('argument callback must be a function');
|
|
}
|
|
|
|
// require the callback without promises
|
|
if (!done && !global.Promise) {
|
|
throw new TypeError('argument callback is required');
|
|
}
|
|
|
|
// get encoding
|
|
var encoding = opts.encoding !== true ? opts.encoding : 'utf-8';
|
|
|
|
// convert the limit to an integer
|
|
var limit = bytes.parse(opts.limit);
|
|
|
|
// convert the expected length to an integer
|
|
var length =
|
|
opts.length != null && !isNaN(opts.length) ?
|
|
parseInt(opts.length, 10)
|
|
: null;
|
|
|
|
if (done) {
|
|
// classic callback style
|
|
return readStream(stream, encoding, length, limit, wrap(done));
|
|
}
|
|
|
|
return new Promise(function executor(resolve, reject) {
|
|
readStream(stream, encoding, length, limit, function onRead(err, buf) {
|
|
if (err) return reject(err);
|
|
resolve(buf);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Halt a stream.
|
|
*
|
|
* @param {Object} stream
|
|
* @private
|
|
*/
|
|
|
|
function halt(stream) {
|
|
// unpipe everything from the stream
|
|
unpipe(stream);
|
|
|
|
// pause stream
|
|
if (typeof stream.pause === 'function') {
|
|
stream.pause();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the data from the stream.
|
|
*
|
|
* @param {object} stream
|
|
* @param {string} encoding
|
|
* @param {number} length
|
|
* @param {number} limit
|
|
* @param {function} callback
|
|
* @public
|
|
*/
|
|
|
|
function readStream(stream, encoding, length, limit, callback) {
|
|
var complete = false;
|
|
var sync = true;
|
|
|
|
// check the length and limit options.
|
|
// note: we intentionally leave the stream paused,
|
|
// so users should handle the stream themselves.
|
|
if (limit !== null && length !== null && length > limit) {
|
|
return done(
|
|
createError(413, 'request entity too large', {
|
|
expected: length,
|
|
length: length,
|
|
limit: limit,
|
|
type: 'entity.too.large',
|
|
})
|
|
);
|
|
}
|
|
|
|
// streams1: assert request encoding is buffer.
|
|
// streams2+: assert the stream encoding is buffer.
|
|
// stream._decoder: streams1
|
|
// state.encoding: streams2
|
|
// state.decoder: streams2, specifically < 0.10.6
|
|
var state = stream._readableState;
|
|
if (stream._decoder || (state && (state.encoding || state.decoder))) {
|
|
// developer error
|
|
return done(
|
|
createError(500, 'stream encoding should not be set', {
|
|
type: 'stream.encoding.set',
|
|
})
|
|
);
|
|
}
|
|
|
|
if (typeof stream.readable !== 'undefined' && !stream.readable) {
|
|
return done(
|
|
createError(500, 'stream is not readable', {
|
|
type: 'stream.not.readable',
|
|
})
|
|
);
|
|
}
|
|
|
|
var received = 0;
|
|
var decoder;
|
|
|
|
try {
|
|
decoder = getDecoder(encoding);
|
|
} catch (err) {
|
|
return done(err);
|
|
}
|
|
|
|
var buffer = decoder ? '' : [];
|
|
|
|
// attach listeners
|
|
stream.on('aborted', onAborted);
|
|
stream.on('close', cleanup);
|
|
stream.on('data', onData);
|
|
stream.on('end', onEnd);
|
|
stream.on('error', onEnd);
|
|
|
|
// mark sync section complete
|
|
sync = false;
|
|
|
|
function done() {
|
|
var args = new Array(arguments.length);
|
|
|
|
// copy arguments
|
|
for (var i = 0; i < args.length; i++) {
|
|
args[i] = arguments[i];
|
|
}
|
|
|
|
// mark complete
|
|
complete = true;
|
|
|
|
if (sync) {
|
|
process.nextTick(invokeCallback);
|
|
} else {
|
|
invokeCallback();
|
|
}
|
|
|
|
function invokeCallback() {
|
|
cleanup();
|
|
|
|
if (args[0]) {
|
|
// halt the stream on error
|
|
halt(stream);
|
|
}
|
|
|
|
callback.apply(null, args);
|
|
}
|
|
}
|
|
|
|
function onAborted() {
|
|
if (complete) return;
|
|
|
|
done(
|
|
createError(400, 'request aborted', {
|
|
code: 'ECONNABORTED',
|
|
expected: length,
|
|
length: length,
|
|
received: received,
|
|
type: 'request.aborted',
|
|
})
|
|
);
|
|
}
|
|
|
|
function onData(chunk) {
|
|
if (complete) return;
|
|
|
|
received += chunk.length;
|
|
|
|
if (limit !== null && received > limit) {
|
|
done(
|
|
createError(413, 'request entity too large', {
|
|
limit: limit,
|
|
received: received,
|
|
type: 'entity.too.large',
|
|
})
|
|
);
|
|
} else if (decoder) {
|
|
buffer += decoder.write(chunk);
|
|
} else {
|
|
buffer.push(chunk);
|
|
}
|
|
}
|
|
|
|
function onEnd(err) {
|
|
if (complete) return;
|
|
if (err) return done(err);
|
|
|
|
if (length !== null && received !== length) {
|
|
done(
|
|
createError(400, 'request size did not match content length', {
|
|
expected: length,
|
|
length: length,
|
|
received: received,
|
|
type: 'request.size.invalid',
|
|
})
|
|
);
|
|
} else {
|
|
var string =
|
|
decoder ? buffer + (decoder.end() || '') : Buffer.concat(buffer);
|
|
done(null, string);
|
|
}
|
|
}
|
|
|
|
function cleanup() {
|
|
buffer = null;
|
|
|
|
stream.removeListener('aborted', onAborted);
|
|
stream.removeListener('data', onData);
|
|
stream.removeListener('end', onEnd);
|
|
stream.removeListener('error', onEnd);
|
|
stream.removeListener('close', cleanup);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try to require async_hooks
|
|
* @private
|
|
*/
|
|
|
|
function tryRequireAsyncHooks() {
|
|
try {
|
|
return require('async_hooks');
|
|
} catch (e) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrap function with async resource, if possible.
|
|
* AsyncResource.bind static method backported.
|
|
* @private
|
|
*/
|
|
|
|
function wrap(fn) {
|
|
var res;
|
|
|
|
// create anonymous resource
|
|
if (asyncHooks.AsyncResource) {
|
|
res = new asyncHooks.AsyncResource(fn.name || 'bound-anonymous-fn');
|
|
}
|
|
|
|
// incompatible node.js
|
|
if (!res || !res.runInAsyncScope) {
|
|
return fn;
|
|
}
|
|
|
|
// return bound function
|
|
return res.runInAsyncScope.bind(res, fn, null);
|
|
}
|