Skip to content

Commit

Permalink
fix: multipart/form-data base64 parsing (nodejs#1668)
Browse files Browse the repository at this point in the history
  • Loading branch information
repsac-by authored and metcoder95 committed Dec 26, 2022
1 parent 2022187 commit 9413576
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 15 deletions.
33 changes: 24 additions & 9 deletions lib/fetch/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,16 +434,31 @@ function bodyMixinMethods (instance) {
})
busboy.on('file', (name, value, info) => {
const { filename, encoding, mimeType } = info
const base64 = encoding.toLowerCase() === 'base64'
const chunks = []
value.on('data', (chunk) => {
if (base64) chunk = Buffer.from(chunk.toString(), 'base64')
chunks.push(chunk)
})
value.on('end', () => {
const file = new File(chunks, filename, { type: mimeType })
responseFormData.append(name, file)
})

if (encoding.toLowerCase() === 'base64') {
let base64chunk = ''

value.on('data', (chunk) => {
base64chunk += chunk.toString().replace(/[\r\n]/gm, '')

const end = base64chunk.length - base64chunk.length % 4
chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64'))

base64chunk = base64chunk.slice(end)
})
value.on('end', () => {
chunks.push(Buffer.from(base64chunk, 'base64'))
responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
})
} else {
value.on('data', (chunk) => {
chunks.push(chunk)
})
value.on('end', () => {
responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
})
}
})

const busboyResolve = new Promise((resolve, reject) => {
Expand Down
19 changes: 13 additions & 6 deletions test/fetch/client-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const nodeFetch = require('../../index-fetch')
const { once } = require('events')
const { gzipSync } = require('zlib')
const { promisify } = require('util')
const { randomFillSync, createHash } = require('crypto')

setGlobalDispatcher(new Agent({
keepAliveTimeout: 1,
Expand Down Expand Up @@ -200,20 +201,26 @@ test('multipart formdata base64', (t) => {
t.plan(1)

// Example form data with base64 encoding
const formRaw = '------formdata-undici-0.5786922755719377\r\nContent-Disposition: form-data; name="key"; filename="test.txt"\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\ndmFsdWU=\r\n------formdata-undici-0.5786922755719377--'
const server = createServer((req, res) => {
const data = randomFillSync(Buffer.alloc(256))
const formRaw = `------formdata-undici-0.5786922755719377\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: base64\r\n\r\n${data.toString('base64')}\r\n------formdata-undici-0.5786922755719377--`
const server = createServer(async (req, res) => {
res.setHeader('content-type', 'multipart/form-data; boundary=----formdata-undici-0.5786922755719377')
res.write(formRaw)

for (let offset = 0; offset < formRaw.length;) {
res.write(formRaw.slice(offset, offset += 2))
await new Promise(resolve => setTimeout(resolve))
}
res.end()
})
t.teardown(server.close.bind(server))

server.listen(0, () => {
fetch(`http://localhost:${server.address().port}`)
.then(res => res.formData())
.then(form => form.get('key').text())
.then(text => {
t.equal(text, 'value')
.then(form => form.get('file').arrayBuffer())
.then(buffer => createHash('sha256').update(Buffer.from(buffer)).digest('base64'))
.then(digest => {
t.equal(createHash('sha256').update(data).digest('base64'), digest)
})
})
})
Expand Down

0 comments on commit 9413576

Please sign in to comment.