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: handling of multipart/form-data requests #173

Merged
merged 19 commits into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"asi": true,
"browser": true,
"node": true
}
105 changes: 105 additions & 0 deletions src/helpers/form-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @license https://raw.githubusercontent.com/node-fetch/node-fetch/master/LICENSE.md
*
* The MIT License (MIT)
*
* Copyright (c) 2016 - 2020 Node Fetch 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
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Extracted from https://github.com/node-fetch/node-fetch/blob/64c5c296a0250b852010746c76144cb9e14698d9/src/utils/form-data.js
*/

erunion marked this conversation as resolved.
Show resolved Hide resolved
const carriage = '\r\n'
const dashes = '-'.repeat(2)

const NAME = Symbol.toStringTag

const isBlob = object => {
return (
typeof object === 'object' &&
typeof object.arrayBuffer === 'function' &&
typeof object.type === 'string' &&
typeof object.stream === 'function' &&
typeof object.constructor === 'function' &&
/^(Blob|File)$/.test(object[NAME])
)
}

/**
* @param {string} boundary
*/
const getFooter = boundary => `${dashes}${boundary}${dashes}${carriage.repeat(2)}`

/**
* @param {string} boundary
* @param {string} name
* @param {*} field
*
* @return {string}
*/
function getHeader (boundary, name, field) {
let header = ''

header += `${dashes}${boundary}${carriage}`
header += `Content-Disposition: form-data; name="${name}"`

if (isBlob(field)) {
header += `; filename="${field.name}"${carriage}`
header += `Content-Type: ${field.type || 'application/octet-stream'}`
}

return `${header}${carriage.repeat(2)}`
}

/**
* @return {string}
*/
module.exports.getBoundary = () => {
// This generates a 50 character boundary similar to those used by Firefox.
// They are optimized for boyer-moore parsing.
var boundary = '--------------------------'
for (var i = 0; i < 24; i++) {
boundary += Math.floor(Math.random() * 10).toString(16)
}

return boundary
}

/**
* @param {FormData} form
* @param {string} boundary
*/
module.exports.formDataIterator = function * (form, boundary) {
for (const [name, value] of form) {
yield getHeader(boundary, name, value)

if (isBlob(value)) {
yield * value.stream()
} else {
yield value
}

yield carriage
}

yield getFooter(boundary)
}

module.exports.isBlob = isBlob
62 changes: 52 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
/* eslint-env browser */

'use strict'

var debug = require('debug')('httpsnippet')
var es = require('event-stream')
var MultiPartForm = require('form-data')
var FormDataPolyfill = require('form-data/lib/form_data')
var qs = require('querystring')
var reducer = require('./helpers/reducer')
var targets = require('./targets')
var url = require('url')
var validate = require('har-validator/lib/async')

const { formDataIterator, isBlob } = require('./helpers/form-data.js')

// constructor
var HTTPSnippet = function (data) {
var entries
Expand Down Expand Up @@ -104,22 +109,59 @@ HTTPSnippet.prototype.prepare = function (request) {
if (request.postData.params) {
var form = new MultiPartForm()

// The `form-data` module returns one of two things: a native FormData object, or its own polyfill. Since the
// polyfill does not support the full API of the native FormData object, when this library is running in a
// browser environment it'll fail on two things:
//
// - The API for `form.append()` has three arguments and the third should only be present when the second is a
// Blob or USVString.
// - `FormData.pipe()` isn't a function.
//
// Since the native FormData object is iterable, we easily detect what version of `form-data` we're working
// with here to allow `multipart/form-data` requests to be compiled under both browser and Node environments.
//
// This hack is pretty awful but it's the only way we can use this library in the browser as if we code this
// against just the native FormData object, we can't polyfill that back into Node because Blob and File objects,
// which something like `formdata-polyfill` requires, don't exist there.
const isNativeFormData = !(form instanceof FormDataPolyfill)

// easter egg
form._boundary = '---011000010111000001101001'
const boundary = '---011000010111000001101001'
if (!isNativeFormData) {
form._boundary = boundary
}

request.postData.params.forEach(function (param) {
form.append(param.name, param.value || '', {
filename: param.fileName || null,
contentType: param.contentType || null
})
const name = param.name
const value = param.value || ''
const filename = param.fileName || null

if (isNativeFormData) {
if (isBlob(value)) {
form.append(name, value, filename)
} else {
form.append(name, value)
}
} else {
form.append(name, value, {
filename: filename,
contentType: param.contentType || null
})
}
})

form.pipe(es.map(function (data, cb) {
request.postData.text += data
}))
if (isNativeFormData) {
for (var data of formDataIterator(form, boundary)) {
request.postData.text += data
}
} else {
form.pipe(es.map(function (data, cb) {
request.postData.text += data
}))
}

request.postData.boundary = form.getBoundary()
request.headersObj['content-type'] = 'multipart/form-data; boundary=' + form.getBoundary()
request.postData.boundary = boundary
request.headersObj['content-type'] = 'multipart/form-data; boundary=' + boundary
}
break

Expand Down
4 changes: 2 additions & 2 deletions src/targets/node/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ module.exports = function (source, options) {
return
}

if (param.fileName && !param.value) {
if (param.fileName) {
includeFS = true

attachment.value = 'fs.createReadStream("' + param.fileName + '")'
Expand Down Expand Up @@ -115,7 +115,7 @@ module.exports = function (source, options) {
.push('});')
.blank()

return code.join().replace('"JAR"', 'jar').replace(/"fs\.createReadStream\(\\"(.+)\\"\)"/, 'fs.createReadStream("$1")')
return code.join().replace('"JAR"', 'jar').replace(/'fs\.createReadStream\("(.+)"\)'/g, "fs.createReadStream('$1')")
develohpanda marked this conversation as resolved.
Show resolved Hide resolved
}

module.exports.info = {
Expand Down
4 changes: 2 additions & 2 deletions src/targets/php/http2.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ module.exports = function (source, options) {

code.push('$body = new http\\Message\\Body;')
.push('$body->addForm(%s, %s);',
Object.keys(fields).length ? helpers.convert(fields, opts.indent) : 'NULL',
files.length ? helpers.convert(files, opts.indent) : 'NULL'
Object.keys(fields).length ? helpers.convert(fields, opts.indent) : 'null',
files.length ? helpers.convert(files, opts.indent) : 'null'
)

// remove the contentType header
Expand Down
7 changes: 4 additions & 3 deletions src/targets/shell/curl.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ module.exports = function (source, options) {
switch (source.postData.mimeType) {
case 'multipart/form-data':
source.postData.params.map(function (param) {
var post = util.format('%s=%s', param.name, param.value)

if (param.fileName && !param.value) {
var post = ''
if (param.fileName) {
develohpanda marked this conversation as resolved.
Show resolved Hide resolved
post = util.format('%s=@%s', param.name, param.fileName)
} else {
post = util.format('%s=%s', param.name, param.value)
}

code.push('%s %s', opts.short ? '-F' : '--form', helpers.quote(post))
Expand Down
10 changes: 5 additions & 5 deletions test/fixtures/output/http/1.1/application-form-encoded
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
POST /har HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: mockbin.com
Content-Length: 19
POST /har HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: mockbin.com
Content-Length: 19

foo=bar&hello=world
10 changes: 5 additions & 5 deletions test/fixtures/output/http/1.1/application-json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
POST /har HTTP/1.1
Content-Type: application/json
Host: mockbin.com
Content-Length: 118
POST /har HTTP/1.1
Content-Type: application/json
Host: mockbin.com
Content-Length: 118

{"number":1,"string":"f\"oo","arr":[1,2,3],"nested":{"a":"b"},"arr_mix":[1,"a",{"arr_mix_nested":{}}],"boolean":false}
8 changes: 4 additions & 4 deletions test/fixtures/output/http/1.1/cookies
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
POST /har HTTP/1.1
Cookie: foo=bar; bar=baz
Host: mockbin.com
POST /har HTTP/1.1
Cookie: foo=bar; bar=baz
Host: mockbin.com


6 changes: 3 additions & 3 deletions test/fixtures/output/http/1.1/custom-method
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PROPFIND /har HTTP/1.1
Host: mockbin.com
PROPFIND /har HTTP/1.1
Host: mockbin.com


14 changes: 7 additions & 7 deletions test/fixtures/output/http/1.1/full
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
POST /har?foo=bar&foo=baz&baz=abc&key=value HTTP/1.1
Cookie: foo=bar; bar=baz
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Host: mockbin.com
Content-Length: 7
POST /har?foo=bar&foo=baz&baz=abc&key=value HTTP/1.1
Cookie: foo=bar; bar=baz
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Host: mockbin.com
Content-Length: 7

foo=bar
10 changes: 5 additions & 5 deletions test/fixtures/output/http/1.1/headers
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
GET /har HTTP/1.1
Accept: application/json
X-Foo: Bar
Host: mockbin.com
GET /har HTTP/1.1
Accept: application/json
X-Foo: Bar
Host: mockbin.com


6 changes: 3 additions & 3 deletions test/fixtures/output/http/1.1/https
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GET /har HTTP/1.1
Host: mockbin.com
GET /har HTTP/1.1
Host: mockbin.com


10 changes: 5 additions & 5 deletions test/fixtures/output/http/1.1/jsonObj-multiline
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
POST /har HTTP/1.1
Content-Type: application/json
Host: mockbin.com
Content-Length: 18
POST /har HTTP/1.1
Content-Type: application/json
Host: mockbin.com
Content-Length: 18

{
"foo": "bar"
}
10 changes: 5 additions & 5 deletions test/fixtures/output/http/1.1/jsonObj-null-value
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
POST /har HTTP/1.1
Content-Type: application/json
Host: mockbin.com
Content-Length: 12
POST /har HTTP/1.1
Content-Type: application/json
Host: mockbin.com
Content-Length: 12

{"foo":null}
19 changes: 10 additions & 9 deletions test/fixtures/output/http/1.1/multipart-data
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
POST /har HTTP/1.1
Content-Type: multipart/form-data; boundary=---011000010111000001101001
Host: mockbin.com
Content-Length: 136
-----011000010111000001101001
Content-Disposition: form-data; name="foo"; filename="hello.txt"
Content-Type: text/plain
POST /har HTTP/1.1
Content-Type: multipart/form-data; boundary=---011000010111000001101001
Host: mockbin.com
Content-Length: 171

-----011000010111000001101001
Content-Disposition: form-data; name="foo"; filename="hello.txt"
Content-Type: text/plain

Hello World
-----011000010111000001101001--
19 changes: 10 additions & 9 deletions test/fixtures/output/http/1.1/multipart-file
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
POST /har HTTP/1.1
Content-Type: multipart/form-data; boundary=---011000010111000001101001
Host: mockbin.com
Content-Length: 125

-----011000010111000001101001
Content-Disposition: form-data; name="foo"; filename="hello.txt"
Content-Type: text/plain

POST /har HTTP/1.1
Content-Type: multipart/form-data; boundary=---011000010111000001101001
Host: mockbin.com
Content-Length: 160

-----011000010111000001101001
Content-Disposition: form-data; name="foo"; filename="hello.txt"
Content-Type: text/plain


-----011000010111000001101001--
Loading