Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: catch file stream error #31

Merged
merged 6 commits into from
May 6, 2023
Merged
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
50 changes: 11 additions & 39 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
@@ -1,46 +1,18 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: Node.js CI

on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
schedule:
- cron: '0 2 * * *'

jobs:
build:
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
node-version: [14, 16]
os: [ubuntu-latest, windows-latest, macos-latest]
branches: [ master ]

steps:
- name: Checkout Git Source
uses: actions/checkout@v2

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}

- name: Install Dependencies
run: npm i -g npminstall@5 && npminstall
pull_request:
branches: [ master ]

- name: Continuous Integration
run: npm run ci
workflow_dispatch: {}

- name: Code Coverage
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
jobs:
Job:
name: Node.js
uses: artusjs/github-actions/.github/workflows/node-test.yml@master
with:
os: 'ubuntu-latest, macos-latest, windows-latest'
version: '14, 16, 18, 20'
67 changes: 31 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
# co busboy

[![NPM version][npm-image]][npm-url]
[![build status][travis-image]][travis-url]
[![Node.js CI](https://github.com/cojs/busboy/actions/workflows/nodejs.yml/badge.svg)](https://github.com/cojs/busboy/actions/workflows/nodejs.yml)
[![Test coverage][codecov-image]][codecov-url]
[![David deps][david-image]][david-url]
[![npm download][download-image]][download-url]

[npm-image]: https://img.shields.io/npm/v/co-busboy.svg?style=flat-square
[npm-url]: https://npmjs.org/package/co-busboy
[travis-image]: https://img.shields.io/travis/cojs/busboy.svg?style=flat-square
[travis-url]: https://travis-ci.org/cojs/busboy
[codecov-image]: https://codecov.io/github/cojs/busboy/coverage.svg?branch=master
[codecov-url]: https://codecov.io/github/cojs/busboy?branch=master
[david-image]: https://img.shields.io/david/cojs/busboy.svg?style=flat-square
[david-url]: https://david-dm.org/cojs/busboy
[download-image]: https://img.shields.io/npm/dm/co-busboy.svg?style=flat-square
[download-url]: https://npmjs.org/package/co-busboy

Expand All @@ -22,15 +17,15 @@
## Example

```js
var parse = require('co-busboy')
const parse = require('co-busboy')

app.use(function* (next) {
app.use(async (next) => {
// the body isn't multipart, so busboy can't parse it
if (!this.request.is('multipart/*')) return yield next
if (!this.request.is('multipart/*')) return await next()

var parts = parse(this)
var part
while (part = yield parts()) {
const parts = parse(this)
let part
while (part = await parts()) {
if (part.length) {
// arrays are busboy fields
console.log('key: ' + part[0])
Expand All @@ -52,14 +47,14 @@ set the `autoFields: true` option.
Now all the parts will be streams and a field object and array will automatically be populated.

```js
var parse = require('co-busboy')
const parse = require('co-busboy')

app.use(function* (next) {
var parts = parse(this, {
app.use(async (next) => {
const parts = parse(this, {
autoFields: true
})
var part
while (part = yield parts()) {
let part
while (part = await parts()) {
// it's a stream
part.pipe(fs.createWriteStream('some file.txt'))
}
Expand All @@ -77,21 +72,21 @@ Use `options.checkField` hook `function(name, val, fieldnameTruncated, valTrunca
can handle fields check.

```js
var parse = require('co-busboy')
const parse = require('co-busboy')

app.use(function* (next) {
var ctx = this
var parts = parse(this, {
checkField: function (name, value) {
app.use(async (next) => {
const ctx = this
const parts = parse(this, {
checkField: (name, value) => {
if (name === '_csrf' && !checkCSRF(ctx, value)) {
var err = new Error('invalid csrf token')
err.status = 400
return err
}
}
})
var part
while (part = yield parts()) {
let part
while (part = await parts()) {
// ...
}
})
Expand All @@ -103,23 +98,23 @@ Use `options.checkFile` hook `function(fieldname, file, filename, encoding, mime
can handle filename check.

```js
var parse = require('co-busboy')
var path = require('path')
const parse = require('co-busboy')
const path = require('path')

app.use(function* (next) {
var ctx = this
var parts = parse(this, {
app.use(async (next) => {
const ctx = this
const parts = parse(this, {
// only allow upload `.jpg` files
checkFile: function (fieldname, file, filename) {
checkFile: (fieldname, file, filename) => {
if (path.extname(filename) !== '.jpg') {
var err = new Error('invalid jpg image')
err.status = 400
return err
}
}
})
var part
while (part = yield parts()) {
let part
while (part = await parts()) {
// ...
}
})
Expand All @@ -130,8 +125,8 @@ app.use(function* (next) {
### parts = parse(stream, [options])

```js
var parse = require('co-busboy')
var parts = parse(stream, {
const parse = require('co-busboy')
const parts = parse(stream, {
autoFields: true
})
```
Expand All @@ -141,9 +136,9 @@ The only additional option is `autoFields`.

**Note**: If busboy events `partsLimit`, `filesLimit`, `fieldsLimit` is emitted, will throw an error.

### part = yield parts()
### part = await parts()

Yield the next part.
Await the next part.
If `autoFields: true`, this will always be a file stream.
Otherwise, it will be a [field](https://github.com/mscdex/busboy#busboy-special-events) as an array.

Expand Down
12 changes: 12 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ module.exports = function (request, options) {
}

function onFile(fieldname, file, info) {
function onFileError(err) {
lastError = err
}
function onFileCleanup() {
file.removeListener('error', onFileError)
file.removeListener('end', onFileCleanup)
file.removeListener('close', onFileCleanup)
}
file.on('error', onFileError)
file.on('end', onFileCleanup)
file.on('close', onFileCleanup)

var filename = info.filename
var encoding = info.encoding
var mimetype = info.mimeType
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"scripts": {
"test": "mocha **/*.test.js",
"lint": "echo 'ignore'",
"ci": "c8 npm test"
},
"files": [
Expand Down
105 changes: 86 additions & 19 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,22 @@ const zlib = require('zlib');
var busboy = require('..')

describe('Co Busboy', function () {
it('should work without autofields', function () {
return co(function*(){
var parts = busboy(request())
var part
var fields = 0
var streams = 0
while (part = yield parts) {
if (part.length) {
assert.equal(part.length, 4)
fields++
} else {
streams++
part.resume()
}
it('should work without autofields', async () => {
const parts = busboy(request())
let part
let fields = 0
let streams = 0
while (part = await parts()) {
if (part.length) {
assert.equal(part.length, 4)
fields++
} else {
streams++
part.resume()
}
assert.equal(fields, 6)
assert.equal(streams, 3)
})
}
assert.equal(fields, 6)
assert.equal(streams, 3)
})

it('should work without autofields on gziped content', function () {
Expand Down Expand Up @@ -262,7 +260,7 @@ describe('Co Busboy', function () {
describe('checkFile()', function() {
var logfile = path.join(__dirname, 'test.log')
before(function() {
fs.writeFileSync(logfile, new Buffer(1024 * 1024 * 10))
fs.writeFileSync(logfile, Buffer.alloc(1024 * 1024 * 10))
})

after(function() {
Expand Down Expand Up @@ -496,6 +494,48 @@ describe('Co Busboy', function () {
})
})
})

describe('invalid multipart', function() {
it('should handle error: Unexpected end of form', function() {
return co(function*(){
var parts = busboy(invalidRequest());
var part;
try {
while (part = yield parts) {
if (!part.length) {
part.resume()
}
}

throw new Error('should not run this')
} catch (err) {
assert.equal(err.message, 'Unexpected end of form')
}
})
})

it('should handle error: Unexpected end of form with checkFile', function() {
return co(function*(){
var parts = busboy(invalidRequest(), {
checkFile: function () {
return new Error('invalid filename extension')
}
});
var part;
try {
while (part = yield parts) {
if (!part.length) {
part.resume()
}
}

throw new Error('should not run this')
} catch (err) {
assert.equal(err.message, 'Unexpected end of form')
}
})
})
})
})

function wait(ms) {
Expand Down Expand Up @@ -559,13 +599,40 @@ function request() {
return stream
}


function gziped() {
// using `gzip` as demo, zlib support `deflate` as well
var stream = request()
const oldHeaders = stream.headers
stream = stream.pipe(zlib.createGzip())
stream.headers = oldHeaders
stream.headers['content-encoding'] = 'gzip'

return stream
}

function invalidRequest() {
// https://github.com/mscdex/busboy/blob/master/test/test-types-multipart.js

var stream = new Stream.PassThrough()

stream.headers = {
'content-type': 'multipart/form-data; boundary=---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k'
}

stream.end([
'-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
'Content-Type: application/octet-stream',
'',
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
'-----------------------------invalid',
'Content-Disposition: form-data; name="upload_file_2"; filename="hack.exe"',
'Content-Type: application/octet-stream',
'',
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
'-----------------------------invalid--'
].join('\r\n'))

return stream
}