Skip to content

Commit

Permalink
Merge pull request #173 from readmeio/fix/multipart-handling
Browse files Browse the repository at this point in the history
  • Loading branch information
develohpanda authored Aug 20, 2020
2 parents 21eee29 + e00ea3a commit 7da7657
Show file tree
Hide file tree
Showing 35 changed files with 313 additions and 146 deletions.
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
*/

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')")
}

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) {
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

0 comments on commit 7da7657

Please sign in to comment.