Skip to content

Commit 7da7657

Browse files
authored
Merge pull request #173 from readmeio/fix/multipart-handling
2 parents 21eee29 + e00ea3a commit 7da7657

35 files changed

+313
-146
lines changed

.jshintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"asi": true,
3+
"browser": true,
34
"node": true
45
}

src/helpers/form-data.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* @license https://raw.githubusercontent.com/node-fetch/node-fetch/master/LICENSE.md
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2016 - 2020 Node Fetch Team
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in all
16+
* copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
* SOFTWARE.
25+
*
26+
* Extracted from https://github.com/node-fetch/node-fetch/blob/64c5c296a0250b852010746c76144cb9e14698d9/src/utils/form-data.js
27+
*/
28+
29+
const carriage = '\r\n'
30+
const dashes = '-'.repeat(2)
31+
32+
const NAME = Symbol.toStringTag
33+
34+
const isBlob = object => {
35+
return (
36+
typeof object === 'object' &&
37+
typeof object.arrayBuffer === 'function' &&
38+
typeof object.type === 'string' &&
39+
typeof object.stream === 'function' &&
40+
typeof object.constructor === 'function' &&
41+
/^(Blob|File)$/.test(object[NAME])
42+
)
43+
}
44+
45+
/**
46+
* @param {string} boundary
47+
*/
48+
const getFooter = boundary => `${dashes}${boundary}${dashes}${carriage.repeat(2)}`
49+
50+
/**
51+
* @param {string} boundary
52+
* @param {string} name
53+
* @param {*} field
54+
*
55+
* @return {string}
56+
*/
57+
function getHeader (boundary, name, field) {
58+
let header = ''
59+
60+
header += `${dashes}${boundary}${carriage}`
61+
header += `Content-Disposition: form-data; name="${name}"`
62+
63+
if (isBlob(field)) {
64+
header += `; filename="${field.name}"${carriage}`
65+
header += `Content-Type: ${field.type || 'application/octet-stream'}`
66+
}
67+
68+
return `${header}${carriage.repeat(2)}`
69+
}
70+
71+
/**
72+
* @return {string}
73+
*/
74+
module.exports.getBoundary = () => {
75+
// This generates a 50 character boundary similar to those used by Firefox.
76+
// They are optimized for boyer-moore parsing.
77+
var boundary = '--------------------------'
78+
for (var i = 0; i < 24; i++) {
79+
boundary += Math.floor(Math.random() * 10).toString(16)
80+
}
81+
82+
return boundary
83+
}
84+
85+
/**
86+
* @param {FormData} form
87+
* @param {string} boundary
88+
*/
89+
module.exports.formDataIterator = function * (form, boundary) {
90+
for (const [name, value] of form) {
91+
yield getHeader(boundary, name, value)
92+
93+
if (isBlob(value)) {
94+
yield * value.stream()
95+
} else {
96+
yield value
97+
}
98+
99+
yield carriage
100+
}
101+
102+
yield getFooter(boundary)
103+
}
104+
105+
module.exports.isBlob = isBlob

src/index.js

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
/* eslint-env browser */
2+
13
'use strict'
24

35
var debug = require('debug')('httpsnippet')
46
var es = require('event-stream')
57
var MultiPartForm = require('form-data')
8+
var FormDataPolyfill = require('form-data/lib/form_data')
69
var qs = require('querystring')
710
var reducer = require('./helpers/reducer')
811
var targets = require('./targets')
912
var url = require('url')
1013
var validate = require('har-validator/lib/async')
1114

15+
const { formDataIterator, isBlob } = require('./helpers/form-data.js')
16+
1217
// constructor
1318
var HTTPSnippet = function (data) {
1419
var entries
@@ -104,22 +109,59 @@ HTTPSnippet.prototype.prepare = function (request) {
104109
if (request.postData.params) {
105110
var form = new MultiPartForm()
106111

112+
// The `form-data` module returns one of two things: a native FormData object, or its own polyfill. Since the
113+
// polyfill does not support the full API of the native FormData object, when this library is running in a
114+
// browser environment it'll fail on two things:
115+
//
116+
// - The API for `form.append()` has three arguments and the third should only be present when the second is a
117+
// Blob or USVString.
118+
// - `FormData.pipe()` isn't a function.
119+
//
120+
// Since the native FormData object is iterable, we easily detect what version of `form-data` we're working
121+
// with here to allow `multipart/form-data` requests to be compiled under both browser and Node environments.
122+
//
123+
// This hack is pretty awful but it's the only way we can use this library in the browser as if we code this
124+
// against just the native FormData object, we can't polyfill that back into Node because Blob and File objects,
125+
// which something like `formdata-polyfill` requires, don't exist there.
126+
const isNativeFormData = !(form instanceof FormDataPolyfill)
127+
107128
// easter egg
108-
form._boundary = '---011000010111000001101001'
129+
const boundary = '---011000010111000001101001'
130+
if (!isNativeFormData) {
131+
form._boundary = boundary
132+
}
109133

110134
request.postData.params.forEach(function (param) {
111-
form.append(param.name, param.value || '', {
112-
filename: param.fileName || null,
113-
contentType: param.contentType || null
114-
})
135+
const name = param.name
136+
const value = param.value || ''
137+
const filename = param.fileName || null
138+
139+
if (isNativeFormData) {
140+
if (isBlob(value)) {
141+
form.append(name, value, filename)
142+
} else {
143+
form.append(name, value)
144+
}
145+
} else {
146+
form.append(name, value, {
147+
filename: filename,
148+
contentType: param.contentType || null
149+
})
150+
}
115151
})
116152

117-
form.pipe(es.map(function (data, cb) {
118-
request.postData.text += data
119-
}))
153+
if (isNativeFormData) {
154+
for (var data of formDataIterator(form, boundary)) {
155+
request.postData.text += data
156+
}
157+
} else {
158+
form.pipe(es.map(function (data, cb) {
159+
request.postData.text += data
160+
}))
161+
}
120162

121-
request.postData.boundary = form.getBoundary()
122-
request.headersObj['content-type'] = 'multipart/form-data; boundary=' + form.getBoundary()
163+
request.postData.boundary = boundary
164+
request.headersObj['content-type'] = 'multipart/form-data; boundary=' + boundary
123165
}
124166
break
125167

src/targets/node/request.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ module.exports = function (source, options) {
6161
return
6262
}
6363

64-
if (param.fileName && !param.value) {
64+
if (param.fileName) {
6565
includeFS = true
6666

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

118-
return code.join().replace('"JAR"', 'jar').replace(/"fs\.createReadStream\(\\"(.+)\\"\)"/, 'fs.createReadStream("$1")')
118+
return code.join().replace('"JAR"', 'jar').replace(/'fs\.createReadStream\("(.+)"\)'/g, "fs.createReadStream('$1')")
119119
}
120120

121121
module.exports.info = {

src/targets/php/http2.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ module.exports = function (source, options) {
6060

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

6767
// remove the contentType header

src/targets/shell/curl.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ module.exports = function (source, options) {
4444
switch (source.postData.mimeType) {
4545
case 'multipart/form-data':
4646
source.postData.params.map(function (param) {
47-
var post = util.format('%s=%s', param.name, param.value)
48-
49-
if (param.fileName && !param.value) {
47+
var post = ''
48+
if (param.fileName) {
5049
post = util.format('%s=@%s', param.name, param.fileName)
50+
} else {
51+
post = util.format('%s=%s', param.name, param.value)
5152
}
5253

5354
code.push('%s %s', opts.short ? '-F' : '--form', helpers.quote(post))
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
POST /har HTTP/1.1
2-
Content-Type: application/x-www-form-urlencoded
3-
Host: mockbin.com
4-
Content-Length: 19
5-
1+
POST /har HTTP/1.1
2+
Content-Type: application/x-www-form-urlencoded
3+
Host: mockbin.com
4+
Content-Length: 19
5+
66
foo=bar&hello=world
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
POST /har HTTP/1.1
2-
Content-Type: application/json
3-
Host: mockbin.com
4-
Content-Length: 118
5-
1+
POST /har HTTP/1.1
2+
Content-Type: application/json
3+
Host: mockbin.com
4+
Content-Length: 118
5+
66
{"number":1,"string":"f\"oo","arr":[1,2,3],"nested":{"a":"b"},"arr_mix":[1,"a",{"arr_mix_nested":{}}],"boolean":false}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
POST /har HTTP/1.1
2-
Cookie: foo=bar; bar=baz
3-
Host: mockbin.com
4-
1+
POST /har HTTP/1.1
2+
Cookie: foo=bar; bar=baz
3+
Host: mockbin.com
4+
55

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
PROPFIND /har HTTP/1.1
2-
Host: mockbin.com
3-
1+
PROPFIND /har HTTP/1.1
2+
Host: mockbin.com
3+
44

0 commit comments

Comments
 (0)