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
4 changes: 1 addition & 3 deletions library/helpers/form-parsing/LICENSE
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
MIT License

Copyright Brian White. All rights reserved.
Copyright (c) 2021-present The Fastify team

The Fastify team members are listed at https://github.com/fastify/fastify#team.
Copyright (c) 2021-present The Fastify team <https://github.com/fastify/fastify#team>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
60 changes: 36 additions & 24 deletions library/helpers/form-parsing/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
# busboy

<div align="center">

[![Build Status](https://github.com/fastify/busboy/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/busboy/actions)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
[![Security Responsible Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow.svg)](https://github.com/fastify/.github/blob/main/SECURITY.md)

</div>

<div align="center">

[![NPM version](https://img.shields.io/npm/v/@fastify/busboy.svg?style=flat)](https://www.npmjs.com/package/@fastify/busboy)
[![NPM downloads](https://img.shields.io/npm/dm/@fastify/busboy.svg?style=flat)](https://www.npmjs.com/package/@fastify/busboy)

</div>
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
[![Security Responsible Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow.svg)](https://github.com/fastify/.github/blob/main/SECURITY.md)

# Description
Description
===========

A Node.js module for parsing incoming HTML form data.

Expand All @@ -29,17 +20,20 @@ Benchmark (Mean time for 500 Kb payload, 2000 cycles, 1000 cycle warmup):
| busboy | 0.3.1 | `340114` |
| @fastify/busboy | 1.0.0 | `270984` |

[Changelog](https://github.com/fastify/busboy/blob/main/CHANGELOG.md) since busboy 0.31.
Requirements
============

# Requirements
- [Node.js](https://nodejs.org/) 10+

- [Node.js](http://nodejs.org/) 10+
Install
=======

# Install

npm i @fastify/busboy
```sh
npm i @fastify/busboy
```

# Examples
Examples
========

- Parsing (multipart) with default options:

Expand Down Expand Up @@ -212,17 +206,29 @@ http
// Done parsing form!
```

# API
API
===

_Busboy_ is a _Writable_ stream

## Busboy (special) events
Busboy (special) events
-----------------------

- **file**(< _string_ >fieldname, < _ReadableStream_ >stream, < _string_ >filename, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new file form field found. `transferEncoding` contains the 'Content-Transfer-Encoding' value for the file stream. `mimeType` contains the 'Content-Type' value for the file stream.
- Note: if you listen for this event, you should always handle the `stream` no matter if you care about the file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents), otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any** incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically and safely discarded (these discarded files do still count towards `files` and `parts` limits).
- If a configured file size limit was reached, `stream` will both have a boolean property `truncated` (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
- The property `bytesRead` informs about the number of bytes that have been read so far.

- **limit**() - Emitted when a file exceeds the configured `fileSize` limit. You can listen on the file stream to handle it:

```js
busboy.on("file", (fieldname, stream) => {
stream.on("limit", () => {
console.log("File size exceeded");
});
});
```

- **field**(< _string_ >fieldname, < _string_ >value, < _boolean_ >fieldnameTruncated, < _boolean_ >valueTruncated, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new non-file field found.

- **partsLimit**() - Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted.
Expand All @@ -231,10 +237,13 @@ _Busboy_ is a _Writable_ stream

- **fieldsLimit**() - Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted.

## Busboy methods
Busboy methods
--------------

- **(constructor)**(< _object_ >config) - Creates and returns a new Busboy instance.

- The constructor takes the following valid `config` settings:

- **headers** - _object_ - These are the HTTP headers of the incoming request, which are used by individual parsers.

- **autoDestroy** - _boolean_ - Whether this stream should automatically call .destroy() on itself after ending. (Default: false).
Expand All @@ -248,15 +257,17 @@ _Busboy_ is a _Writable_ stream
- **preservePath** - _boolean_ - If paths in the multipart 'filename' field shall be preserved. (Default: false).

- **isPartAFile** - **function** - Use this function to override the default file detection functionality. It has following parameters:

- fieldName - **string** The name of the field.

- contentType - **string** The content-type of the part, e.g. `text/plain`, `image/jpeg`, `application/octet-stream`

- fileName - **string** The name of a file supplied by the part.

(Default: `(fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)`)
(Default: `(fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)`)

- **limits** - _object_ - Various limits on incoming data. Valid properties are:

- **fieldNameSize** - _integer_ - Max field name size (in bytes) (Default: 100 bytes).

- **fieldSize** - _integer_ - Max field value size (in bytes) (Default: 1 MiB, which is 1024 x 1024 bytes).
Expand All @@ -274,6 +285,7 @@ _Busboy_ is a _Writable_ stream
- **headerSize** - _integer_ - For multipart forms, the max size of a multipart header **Default:** 81920.

- The constructor can throw errors:

- **Busboy expected an options-Object.** - Busboy expected an Object as first parameters.

- **Busboy expected an options-Object with headers-attribute.** - The first parameter is lacking of a headers-attribute.
Expand Down
145 changes: 99 additions & 46 deletions library/helpers/form-parsing/deps/dicer/lib/HeaderParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,101 @@ const EventEmitter = require("node:events").EventEmitter;
const inherits = require("node:util").inherits;
const getLimit = require("../../../lib/utils/getLimit");

const StreamSearch = require("../../streamsearch/sbmh");

const B_DCRLF = Buffer.from("\r\n\r\n");
const RE_CRLF = /\r\n/g;
const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/; // eslint-disable-line no-control-regex
const S_DCRLF = "\r\n\r\n";

function HeaderParser(cfg) {
EventEmitter.call(this);

cfg = cfg || {};
const self = this;
this.nread = 0;
this.maxed = false;
this.npairs = 0;
this.maxHeaderPairs = getLimit(cfg, "maxHeaderPairs", 2000);
this.maxHeaderSize = getLimit(cfg, "maxHeaderSize", 80 * 1024);
this.buffer = "";
this.header = {};
this.header = Object.create(null);
this.finished = false;
this.ss = new StreamSearch(B_DCRLF);
this.ss.on("info", function (isMatch, data, start, end) {
if (data && !self.maxed) {
if (self.nread + end - start >= self.maxHeaderSize) {
end = self.maxHeaderSize - self.nread + start;
self.nread = self.maxHeaderSize;
self.maxed = true;
} else {
self.nread += end - start;
this.tail = "";
}
inherits(HeaderParser, EventEmitter);

HeaderParser.prototype.push = function (data) {
if (!Buffer.isBuffer(data)) {
data = Buffer.from(data, "binary");
}

let end = data.length;
let appendEnd = data.length;
let found = false;
const tail = this.tail;

for (let i = tail.length; i > 0; --i) {
if (tail.endsWith(S_DCRLF.slice(0, i))) {
let matched = data.length >= S_DCRLF.length - i;
for (let j = i; matched && j < S_DCRLF.length; ++j) {
matched = data[j - i] === S_DCRLF.charCodeAt(j);
}
if (matched) {
end = S_DCRLF.length - i;
appendEnd = 0;
found = true;
break;
}
}
}

self.buffer += data.toString("binary", start, end);
if (!found) {
const pos = data.indexOf(B_DCRLF);
if (pos !== -1) {
end = pos + B_DCRLF.length;
appendEnd = pos;
found = true;
}
if (isMatch) {
self._finish();
}

if (!found) {
this.tail =
data.length >= 3
? data.toString("binary", data.length - 3)
: (tail + data.toString("binary")).slice(-3);
} else {
this.tail = "";
}

if (appendEnd !== 0 && !this.maxed) {
const remaining = this.maxHeaderSize - this.nread;
if (appendEnd >= remaining) {
this.buffer += data.toString("binary", 0, remaining);
this.nread = this.maxHeaderSize;
this.maxed = true;
} else {
this.buffer += data.toString("binary", 0, appendEnd);
this.nread += appendEnd;
}
});
}
inherits(HeaderParser, EventEmitter);
}

HeaderParser.prototype.push = function (data) {
const r = this.ss.push(data);
if (this.finished) {
return r;
if (found) {
this._finish();
return end;
}
};

HeaderParser.prototype.reset = function () {
this.finished = false;
this.buffer = "";
this.header = {};
this.ss.reset();
this.header = Object.create(null);
this.tail = "";
};
Comment thread
aikido-autofix[bot] marked this conversation as resolved.

HeaderParser.prototype._finish = function () {
if (this.buffer) {
this._parseHeader();
}
this.ss.matches = this.ss.maxMatches;
const header = this.header;
this.header = {};
this.header = Object.create(null);
this.buffer = "";
this.tail = "";
this.finished = true;
this.nread = this.npairs = 0;
this.maxed = false;
Expand All @@ -76,36 +110,55 @@ HeaderParser.prototype._parseHeader = function () {
return;
}

const lines = this.buffer.split(RE_CRLF);
const len = lines.length;
let m, h;
const buffer = this.buffer;
let h;
let lineStart = 0;

for (var i = 0; i < len; ++i) {
// eslint-disable-line no-var
if (lines[i].length === 0) {
while (lineStart < buffer.length) {
let lineEnd = buffer.indexOf("\r\n", lineStart);
if (lineEnd === -1) {
lineEnd = buffer.length;
}

if (lineEnd === lineStart) {
lineStart = lineEnd + 2;
continue;
}
if (lines[i][0] === "\t" || lines[i][0] === " ") {

// Reject bare line breaks instead of exposing them in parsed values
const line = buffer.slice(lineStart, lineEnd);
if (line.includes("\r") || line.includes("\n")) {
return;
}

if ((buffer[lineStart] === "\t" || buffer[lineStart] === " ") && h) {
// folded header content
// RFC2822 says to just remove the CRLF and not the whitespace following
// it, so we follow the RFC and include the leading whitespace ...
if (h) {
this.header[h][this.header[h].length - 1] += lines[i];
continue;
}
this.header[h][this.header[h].length - 1] += buffer.slice(
lineStart,
lineEnd
);
lineStart = lineEnd + 2;
continue;
}

const posColon = lines[i].indexOf(":");
if (posColon === -1 || posColon === 0) {
const posColon = buffer.indexOf(":", lineStart);
if (posColon === -1 || posColon === lineStart || posColon > lineEnd) {
return;
}
m = RE_HDR.exec(lines[i]);
h = m[1].toLowerCase();
this.header[h] = this.header[h] || [];
this.header[h].push(m[2] || "");
h = buffer.slice(lineStart, posColon).toLowerCase();
let valueStart = posColon + 1;
if (buffer[valueStart] === " " || buffer[valueStart] === "\t") {
++valueStart;
}
const values = this.header[h] || (this.header[h] = []);
values.push(buffer.slice(valueStart, lineEnd));
if (++this.npairs === this.maxHeaderPairs) {
break;
}

lineStart = lineEnd + 2;
}
};

Expand Down
2 changes: 1 addition & 1 deletion library/helpers/form-parsing/deps/streamsearch/sbmh.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function SBMH(needle) {
this.maxMatches = Infinity;
this.matches = 0;

this._occ = new Uint8Array(256).fill(needleLength); // Initialize occurrence table.
this._occ = new Uint16Array(256).fill(needleLength); // Initialize occurrence table.
this._lookbehind_size = 0;
this._needle = needle;
this._bufpos = 0;
Expand Down
Loading
Loading