Skip to content

Commit dc846b1

Browse files
authored
Handle chunked base64 transfer encoding (#281)
1 parent dc48fec commit dc846b1

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

‎index.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,13 @@ Form.prototype.onParseHeaderEnd = function() {
500500

501501
Form.prototype.onParsePartData = function(b) {
502502
if (this.partTransferEncoding === 'base64') {
503-
this.backpressure = ! this.destStream.write(b.toString('ascii'), 'base64');
503+
// Count base64 characters, not whitespace, and retain incomplete groups.
504+
var data = this.partRemainder + b.toString('binary').replace(/[\t\n\v\f\r ]/g, '');
505+
var length = data.length - (data.length % 4);
506+
this.partRemainder = data.slice(length);
507+
if (length > 0) {
508+
this.backpressure = ! this.destStream.write(data.slice(0, length), this.partTransferEncoding);
509+
}
504510
} else {
505511
this.backpressure = ! this.destStream.write(b);
506512
}
@@ -510,8 +516,10 @@ Form.prototype.onParsePartEnd = function() {
510516
if (this.destStream) {
511517
flushWriteCbs(this);
512518
var s = this.destStream;
519+
var remainder = this.partRemainder;
520+
var encoding = this.partTransferEncoding;
513521
process.nextTick(function() {
514-
s.end();
522+
s.end(remainder, encoding);
515523
});
516524
}
517525
clearPartVars(this);
@@ -812,6 +820,7 @@ function clearPartVars(self) {
812820
self.partName = null;
813821
self.partFilename = null;
814822
self.partTransferEncoding = 'binary';
823+
self.partRemainder = '';
815824
self.destStream = null;
816825

817826
self.headerFieldDecoder = new StringDecoder(self.encoding);

‎test/test.js‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,6 +1701,92 @@ describe('multiparty', function () {
17011701
rimraf(TMP_PATH, done)
17021702
})
17031703

1704+
describe('content transfer encoding (issue 280)', function () {
1705+
var cases = [
1706+
{ encoding: 'base64', chunkSize: 0 },
1707+
{ encoding: 'binary', chunkSize: 5 },
1708+
{ encoding: 'base64', chunkSize: 4 },
1709+
{ encoding: 'base64', chunkSize: 1 },
1710+
{ encoding: 'base64', chunkSize: 2 },
1711+
{ encoding: 'base64', chunkSize: 3 },
1712+
{ encoding: 'base64', chunkSize: 5 },
1713+
{ encoding: 'base64', chunkSize: 6 },
1714+
{ encoding: 'base64', chunkSize: 7 },
1715+
{ encoding: 'base64', chunkSize: 5, variant: 'line-wrapped' },
1716+
{ encoding: 'base64', chunkSize: 5, variant: 'unpadded', size: 19 },
1717+
{ encoding: 'base64', chunkSize: 5, variant: 'unpadded', size: 20 }
1718+
]
1719+
1720+
cases.forEach(function (testCase) {
1721+
var chunks = testCase.chunkSize ? testCase.chunkSize + '-byte chunks' : 'one chunk'
1722+
var variant = testCase.variant ? ' (' + testCase.variant + ')' : ''
1723+
if (testCase.size) variant += ' with ' + testCase.size + ' original bytes'
1724+
1725+
it('preserves a ' + testCase.encoding + ' file sent in ' + chunks + variant, function (done) {
1726+
var original = Buffer.from('000102037f80feff48656c6c6f20776f726c6421', 'hex')
1727+
if (testCase.size) original = original.slice(0, testCase.size)
1728+
var body = testCase.encoding === 'base64'
1729+
? Buffer.from(original.toString('base64'))
1730+
: original
1731+
if (testCase.variant === 'line-wrapped') {
1732+
body = Buffer.from(body.toString().replace(/(.{4})/g, '$1\r\n \t'))
1733+
} else if (testCase.variant === 'unpadded') {
1734+
body = Buffer.from(body.toString().replace(/=+$/, ''))
1735+
}
1736+
var header = Buffer.from(
1737+
'--foo\r\n' +
1738+
'Content-Disposition: form-data; name="file"; filename="file.bin"\r\n' +
1739+
'Content-Type: application/octet-stream\r\n' +
1740+
'Content-Transfer-Encoding: ' + testCase.encoding + '\r\n\r\n'
1741+
)
1742+
var footer = Buffer.from('\r\n--foo--\r\n')
1743+
var req = new stream.PassThrough()
1744+
var form = new multiparty.Form({ uploadDir: TMP_PATH })
1745+
1746+
req.headers = {
1747+
'content-type': 'multipart/form-data; boundary=foo',
1748+
'content-length': header.length + body.length + footer.length
1749+
}
1750+
1751+
form.parse(req, function (err, fields, files) {
1752+
if (err) return done(err)
1753+
fs.readFile(files.file[0].path, function (err, actual) {
1754+
if (err) return done(err)
1755+
assert.deepEqual(actual, original)
1756+
done()
1757+
})
1758+
})
1759+
1760+
// Preserve explicit request chunk boundaries without TCP buffering.
1761+
req.write(header)
1762+
var chunkSize = testCase.chunkSize || body.length
1763+
for (var offset = 0; offset < body.length; offset += chunkSize) {
1764+
req.write(body.slice(offset, offset + chunkSize))
1765+
}
1766+
req.end(footer)
1767+
})
1768+
})
1769+
1770+
it('flushes and resets the base64 remainder between parts', function (done) {
1771+
var req = new stream.PassThrough()
1772+
var form = new multiparty.Form()
1773+
req.headers = { 'content-type': 'multipart/form-data; boundary=foo' }
1774+
1775+
form.parse(req, function (err, fields) {
1776+
if (err) return done(err)
1777+
assert.deepEqual(fields, { first: ['a'], second: ['bc'], third: ['plain'] })
1778+
done()
1779+
})
1780+
1781+
req.write('--foo\r\nContent-Disposition: form-data; name="first"\r\n' +
1782+
'Content-Transfer-Encoding: base64\r\n\r\nYQ')
1783+
req.write('\r\n--foo\r\nContent-Disposition: form-data; name="second"\r\n' +
1784+
'Content-Transfer-Encoding: base64\r\n\r\nYmM')
1785+
req.end('\r\n--foo\r\nContent-Disposition: form-data; name="third"\r\n' +
1786+
'\r\nplain\r\n--foo--\r\n')
1787+
})
1788+
})
1789+
17041790
describe('fixture tests', function () {
17051791
var fixtureServer = http.createServer()
17061792
var fixtureTests = requireAll(path.join(FIXTURE_PATH, 'js'))

0 commit comments

Comments
 (0)