Skip to content
Open
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
64 changes: 61 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ and `toString` may not be a function and instead a string or other user input.

[Learn about the anatomy of an HTTP transaction in Node.js](https://nodejs.org/en/learn/http/anatomy-of-an-http-transaction).

_This does not handle multipart bodies_, due to their complex and typically
large nature. For multipart bodies, you may be interested in the following
modules:
_This module provides basic multipart/form-data support for text fields only._
File fields are automatically dropped. For full file upload support, you may be
interested in the following modules:

* [busboy](https://www.npmjs.com/package/busboy#readme) and
[connect-busboy](https://www.npmjs.com/package/connect-busboy#readme)
Expand All @@ -33,6 +33,7 @@ modules:
This module provides the following parsers:

* [JSON body parser](#bodyparserjsonoptions)
* [Multipart/form-data body parser](#bodyparsermultipartoptions)
* [Raw body parser](#bodyparserrawoptions)
* [Text body parser](#bodyparsertextoptions)
* [URL-encoded form body parser](#bodyparserurlencodedoptions)
Expand Down Expand Up @@ -300,6 +301,54 @@ form. Defaults to `false`.

The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible.

### bodyParser.multipart([options])

Returns middleware that only parses `multipart/form-data` bodies and only looks at
requests where the `Content-Type` header matches the `type` option. This parser
extracts text fields and automatically drops file fields. It supports automatic
inflation of `gzip`, `br` (brotli) and `deflate` encodings.

A new `body` object containing the parsed data is populated on the `request`
object after the middleware (i.e. `req.body`). This object will contain
key-value pairs for text fields only. File fields (fields with `filename` in
their `Content-Disposition` header) are automatically dropped.

#### Options

The `multipart` function takes an optional `options` object that may contain
any of the following keys:

##### inflate

When set to `true`, then deflated (compressed) bodies will be inflated; when
`false`, deflated bodies are rejected. Defaults to `true`.

##### limit

Controls the maximum size of individual text fields. If this is a number, then
the value specifies the number of bytes; if it is a string, the value is passed
to the [bytes](https://www.npmjs.com/package/bytes) library for parsing.
Defaults to `'100kb'`. Note: The overall body size limit is automatically set
higher to allow multiple fields.

##### type

The `type` option is used to determine what media type the middleware will
parse. This option can be a string, array of strings, or a function. If not
a function, `type` option is passed directly to the
[type-is](https://www.npmjs.com/package/type-is#readme) library and this can
be an extension name (like `multipart`), a mime type (like
`multipart/form-data`), or a mime type with a wildcard (like `multipart/*`).
If a function, the `type` option is called as `fn(req)` and the request is parsed
if it returns a truthy value. Defaults to `multipart/form-data`.

##### verify

The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
where `buf` is a string containing the field value and `encoding` is the
encoding of the request. The verification is called for each text field
individually. The parsing can be aborted by throwing an error.

## Errors

The middlewares provided by this module create errors using the
Expand Down Expand Up @@ -445,12 +494,21 @@ const jsonParser = bodyParser.json()
// create application/x-www-form-urlencoded parser
const urlencodedParser = bodyParser.urlencoded()

// create multipart/form-data parser
const multipartParser = bodyParser.multipart()

// POST /login gets urlencoded bodies
app.post('/login', urlencodedParser, function (req, res) {
if (!req.body || !req.body.username) res.sendStatus(400)
res.send('welcome, ' + req.body.username)
})

// POST /upload gets multipart bodies (text fields only, files are dropped)
app.post('/upload', multipartParser, function (req, res) {
if (!req.body || !req.body.description) res.sendStatus(400)
res.send('uploaded: ' + req.body.description)
})

// POST /api/users gets JSON bodies
app.post('/api/users', jsonParser, function (req, res) {
if (!req.body) res.sendStatus(400)
Expand Down
12 changes: 12 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/**
* @typedef {Object} Parsers
* @property {Function} json JSON parser
* @property {Function} multipart Multipart/form-data parser
* @property {Function} raw Raw parser
* @property {Function} text Text parser
* @property {Function} urlencoded URL-encoded parser
Expand Down Expand Up @@ -60,6 +61,17 @@ Object.defineProperty(exports, 'urlencoded', {
get: () => require('./lib/types/urlencoded')
})

/**
* Multipart/form-data parser.
* Only extracts text fields and drops file fields.
* @public
*/
Object.defineProperty(exports, 'multipart', {
configurable: true,
enumerable: true,
get: () => require('./lib/types/multipart')
})

/**
* Create a middleware to parse json and urlencoded bodies.
*
Expand Down
198 changes: 198 additions & 0 deletions lib/types/multipart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*!
* body-parser
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/

'use strict'

/**
* Module dependencies.
* @private
*/

var createError = require('http-errors')
var debug = require('debug')('body-parser:multipart')
var read = require('../read')
var { normalizeOptions } = require('../utils')

/**
* Module exports.
*/

module.exports = multipart

/**
* Create a middleware to parse multipart/form-data bodies.
* This parser only extracts text fields and drops file fields.
*
* @param {Object} [options]
* @returns {Function}
* @public
*/
function multipart (options) {
const normalizedOptions = normalizeOptions(options, 'multipart/form-data')

var limit = normalizedOptions.limit
var verify = normalizedOptions.verify

function parse (body, encoding) {
var req = this
if (!body || body.length === 0) {
return {}
}

var contentType = req.headers && req.headers['content-type']
if (!contentType) {
throw createError(400, 'missing content-type header', {
type: 'multipart.content-type.missing'
})
}

if (!contentType.toLowerCase().includes('multipart')) {
debug('non-multipart content-type in parse function - should have been skipped')
return undefined
}

var boundary = extractBoundary(contentType)
var bodyStr = typeof body === 'string' ? body : body.toString('utf-8')
var parts = bodyStr.split('--' + boundary)
var result = {}

for (var i = 1; i < parts.length - 1; i++) {
var field = parsePart(parts[i], limit, req, encoding)
if (field) {
addField(result, field.name, field.value)
}
}

return result
}

var readLimit = normalizedOptions.limit
var overallLimit = Math.max(readLimit * 100, 100 * 1024 * 1024)

const readOptions = {
...normalizedOptions,
limit: overallLimit,
skipCharset: true,
verify: false
}

return function multipartParser (req, res, next) {
req._multipartVerify = verify
read(req, res, next, parse.bind(req), debug, readOptions)
}
}

/**
* Extract boundary from content-type header.
*
* @param {string} contentType
* @returns {string}
* @private
*/
function extractBoundary (contentType) {
var boundaryMatch = contentType.match(/boundary=([^;]+)/i)
if (!boundaryMatch) {
throw createError(400, 'missing boundary in content-type', {
type: 'multipart.boundary.missing'
})
}
return boundaryMatch[1].replace(/^["']|["']$/g, '')
}

/**
* Parse a single multipart part.
*
* @param {string} part
* @param {number} limit
* @param {Object} req
* @param {string} encoding
* @returns {Object|null}
* @private
*/
function parsePart (part, limit, req, encoding) {
var trimmed = part.trim()
if (trimmed === '--' || trimmed === '') {
return null
}

var headerEnd = trimmed.indexOf('\r\n\r\n')
if (headerEnd === -1) {
headerEnd = trimmed.indexOf('\n\n')
if (headerEnd === -1) {
debug('invalid part format')
return null
}
headerEnd += 1
} else {
headerEnd += 4
}

var headers = trimmed.substring(0, headerEnd)
var bodyContent = trimmed.substring(headerEnd).replace(/\r\n$/, '')

var contentDisposition = headers.match(/Content-Disposition:\s*([^\r\n]+)/i)
if (!contentDisposition) {
debug('missing Content-Disposition header')
return null
}

var disposition = contentDisposition[1]

if (/filename\s*=/i.test(disposition)) {
debug('dropping file field')
return null
}

var nameMatch = disposition.match(/name\s*=\s*"([^"]+)"|name\s*=\s*([^;,\s]+)/i)
if (!nameMatch) {
debug('missing field name')
return null
}

var fieldName = nameMatch[1] || nameMatch[2]

if (bodyContent.length > limit) {
var err = createError(413, 'field size limit exceeded', {
type: 'entity.too.large',
limit: limit
})
err.expose = true
throw err
}

var fieldVerify = req._multipartVerify
if (fieldVerify) {
try {
fieldVerify(req, null, bodyContent, encoding || 'utf-8')
} catch (err) {
throw createError(403, err, {
type: err.type || 'entity.verify.failed'
})
}
}

return { name: fieldName, value: bodyContent }
}

/**
* Add field to result object, handling multiple values.
*
* @param {Object} result
* @param {string} name
* @param {string} value
* @private
*/
function addField (result, name, value) {
if (result[name]) {
if (Array.isArray(result[name])) {
result[name].push(value)
} else {
result[name] = [result[name], value]
}
} else {
result[name] = value
}
}
Loading