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

215 lines
7.9 KiB
JavaScript

// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var errors = require('restify-errors');
var bodyReader = require('./bodyReader');
var jsonParser = require('./jsonBodyParser');
var formParser = require('./formBodyParser');
var multipartParser = require('./multipartBodyParser');
var fieldedTextParser = require('./fieldedTextBodyParser.js');
var regex = require('./utils/regex');
///--- Globals
var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;
///--- API
/**
* Blocks your chain on reading and parsing the HTTP request body. Switches on
* `Content-Type` and does the appropriate logic. `application/json`,
* `application/x-www-form-urlencoded` and `multipart/form-data` are currently
* supported.
*
* Parses `POST` bodies to `req.body`. automatically uses one of the following
* parsers based on content type:
* - `urlEncodedBodyParser(options)` - parses url encoded form bodies
* - `jsonBodyParser(options)` - parses JSON POST bodies
* - `multipartBodyParser(options)` - parses multipart form bodies
*
* All bodyParsers support the following options:
* - `options.mapParams` - default false. copies parsed post body values onto
* req.params
* - `options.overrideParams` - default false. only applies when if
* mapParams true. when true, will stomp on req.params value when
* existing value is found.
*
* @public
* @function bodyParser
* @throws {UnsupportedMediaTypeError}
* @param {Object} [options] - an option object
* @param {Number} [options.maxBodySize] - The maximum size in bytes allowed in
* the HTTP body. Useful for limiting clients from hogging server memory.
* @param {Boolean} [options.mapParams] - if `req.params` should be filled with
* parsed parameters from HTTP body.
* @param {Boolean} [options.mapFiles] - if `req.params` should be filled with
* the contents of files sent through a multipart request.
* [formidable](https://github.com/felixge/node-formidable) is used internally
* for parsing, and a file is denoted as a multipart part with the `filename`
* option set in its `Content-Disposition`. This will only be performed if
* `mapParams` is true.
* @param {Boolean} [options.overrideParams] - if an entry in `req.params`
* should be overwritten by the value in the body if the names are the same.
* For instance, if you have the route `/:someval`,
* and someone posts an `x-www-form-urlencoded`
* Content-Type with the body `someval=happy` to `/sad`, the value will be
* `happy` if `overrideParams` is `true`, `sad` otherwise.
* @param {Function} [options.multipartHandler] - a callback to handle any
* multipart part which is not a file.
* If this is omitted, the default handler is invoked which may
* or may not map the parts into `req.params`, depending on
* the `mapParams`-option.
* @param {Function} [options.multipartFileHandler] - a callback to handle any
* multipart file.
* It will be a file if the part has a `Content-Disposition` with the
* `filename` parameter set. This typically happens when a browser sends a
* form and there is a parameter similar to `<input type="file" />`.
* If this is not provided, the default behaviour is to map the contents
* into `req.params`.
* @param {Boolean} [options.keepExtensions] - if you want the uploaded
* files to include the extensions of the original files
* (multipart uploads only).
* Does nothing if `multipartFileHandler` is defined.
* @param {String} [options.uploadDir] - Where uploaded files are
* intermediately stored during transfer before the contents is mapped
* into `req.params`.
* Does nothing if `multipartFileHandler` is defined.
* @param {Boolean} [options.multiples] - if you want to support html5 multiple
* attribute in upload fields.
* @param {String} [options.hash] - If you want checksums calculated for
* incoming files, set this to either `sha1` or `md5`.
* @param {Boolean} [options.rejectUnknown] - Set to `true` if you want to end
* the request with a `UnsupportedMediaTypeError` when none of
* the supported content types was given.
* @param {Boolean} [options.requestBodyOnGet=false] - Parse body of a GET
* request.
* @param {Function} [options.reviver] - `jsonParser` only. If a function,
* this prescribes how the value originally produced by parsing is transformed,
* before being returned. For more information check out
* `JSON.parse(text[, reviver])`.
* @param {Number} [options.maxFieldsSize=2 * 1024 * 1024] - `multipartParser`
* only.
* Limits the amount of memory all fields together (except files)
* can allocate in bytes.
* The default size is `2 * 1024 * 1024` bytes *(2MB)*.
* @returns {Function} Handler
* @example
* server.use(restify.plugins.bodyParser({
* maxBodySize: 0,
* mapParams: true,
* mapFiles: false,
* overrideParams: false,
* multipartHandler: function(part) {
* part.on('data', function(data) {
* // do something with the multipart data
* });
* },
* multipartFileHandler: function(part) {
* part.on('data', function(data) {
* // do something with the multipart file data
* });
* },
* keepExtensions: false,
* uploadDir: os.tmpdir(),
* multiples: true,
* hash: 'sha1',
* rejectUnknown: true,
* requestBodyOnGet: false,
* reviver: undefined,
* maxFieldsSize: 2 * 1024 * 1024
* }));
*/
function bodyParser(options) {
assert.optionalObject(options, 'options');
var opts = options || {};
opts.bodyReader = true;
var read = bodyReader(opts);
var parseForm = formParser(opts);
var parseJson = jsonParser(opts);
var parseMultipart = multipartParser(opts);
var parseFieldedText = fieldedTextParser(opts);
function parseBody(req, res, next) {
// #100 don't parse the body again if we've read it once
if (req._parsedBody) {
next();
return;
} else {
req._parsedBody = true;
}
// Allow use of 'requestBodyOnGet' flag to allow for merging of
// the request body of a GET request into req.params
if (req.method === 'HEAD') {
next();
return;
}
if (req.method === 'GET') {
if (!opts.requestBodyOnGet) {
next();
return;
}
}
if (req.contentLength() === 0 && !req.isChunked()) {
next();
return;
}
var parser;
var type = req.contentType().toLowerCase();
switch (type) {
case 'application/json':
parser = parseJson[0];
break;
case 'application/x-www-form-urlencoded':
parser = parseForm[0];
break;
case 'multipart/form-data':
parser = parseMultipart;
break;
case 'text/tsv':
parser = parseFieldedText;
break;
case 'text/tab-separated-values':
parser = parseFieldedText;
break;
case 'text/csv':
parser = parseFieldedText;
break;
default:
break;
}
// if we find no matches from the direct string comparisons, perform
// more expensive regex matches. map any +json to application/json.
// theoretically these could be mapped to application/json prior to the
// switch statement, but putting it here allows us to skip the regex
// entirely unless absolutely necessary. additional types could be
// added later at some point.
if (!parser) {
if (regex.jsonContentType.test(type)) {
parser = parseJson[0];
}
}
if (parser) {
parser(req, res, next);
} else if (opts && opts.rejectUnknown) {
next(new UnsupportedMediaTypeError(type));
} else {
next();
}
}
return [read, parseBody];
}
module.exports = bodyParser;