2023-10-25 18:45:16 +08:00
|
|
|
import crypto from 'node:crypto'
|
|
|
|
import fs from 'node:fs'
|
2023-10-27 19:16:32 +08:00
|
|
|
import { join } from 'node:path'
|
2023-10-25 18:45:16 +08:00
|
|
|
import { EventEmitter } from 'node:events'
|
2020-09-16 20:07:28 +08:00
|
|
|
|
2023-10-26 19:02:46 +08:00
|
|
|
import File from './file.js'
|
2020-09-16 20:07:28 +08:00
|
|
|
import { MultipartParser } from './multipart_parser.js'
|
2023-10-27 19:16:32 +08:00
|
|
|
import { UrlencodedParser } from './urlencoded_parser.js'
|
2024-07-10 16:44:17 +08:00
|
|
|
import { OctetParser, BufferParser, EmptyParser } from './octet_parser.js'
|
2020-09-16 20:07:28 +08:00
|
|
|
import { JSONParser } from './json_parser.js'
|
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
function randomPath(uploadDir) {
|
|
|
|
var name = 'upload_' + crypto.randomBytes(16).toString('hex')
|
|
|
|
return join(uploadDir, name)
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
function parseFilename(headerValue) {
|
|
|
|
let matches = headerValue.match(/\bfilename="(.*?)"($|; )/i)
|
|
|
|
if (!matches) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let filename = matches[1].slice(matches[1].lastIndexOf('\\') + 1)
|
|
|
|
filename = filename.replace(/%22/g, '"')
|
|
|
|
filename = filename.replace(/&#([\d]{4});/g, function (m, code) {
|
|
|
|
return String.fromCharCode(code)
|
|
|
|
})
|
|
|
|
return filename
|
|
|
|
}
|
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
/* ------------------------------------- */
|
|
|
|
|
2023-10-26 19:02:46 +08:00
|
|
|
export default class IncomingForm extends EventEmitter {
|
|
|
|
#req = null
|
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#error = false
|
|
|
|
#ended = false
|
|
|
|
|
2023-10-26 19:02:46 +08:00
|
|
|
ended = false
|
|
|
|
headers = null
|
2023-10-25 18:45:16 +08:00
|
|
|
|
2023-10-26 19:02:46 +08:00
|
|
|
bytesReceived = null
|
|
|
|
bytesExpected = null
|
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#parser = null
|
2023-10-30 16:41:37 +08:00
|
|
|
#pending = 0
|
2023-10-27 19:16:32 +08:00
|
|
|
|
|
|
|
#openedFiles = []
|
2023-10-26 19:02:46 +08:00
|
|
|
|
|
|
|
constructor(req, opts = {}) {
|
|
|
|
super()
|
|
|
|
|
|
|
|
this.#req = req
|
2023-10-25 18:45:16 +08:00
|
|
|
|
|
|
|
this.uploadDir = opts.uploadDir
|
|
|
|
this.encoding = opts.encoding || 'utf-8'
|
|
|
|
|
2023-10-30 16:59:54 +08:00
|
|
|
this.headers = req.headers
|
|
|
|
this.#parseContentLength()
|
|
|
|
this.#parseContentType()
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-25 18:45:16 +08:00
|
|
|
req
|
2023-10-26 19:02:46 +08:00
|
|
|
.on('error', err => {
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#handleError(err)
|
|
|
|
this.#clearUploads()
|
2023-10-25 18:45:16 +08:00
|
|
|
})
|
2023-10-26 19:02:46 +08:00
|
|
|
.on('aborted', () => {
|
|
|
|
this.emit('aborted')
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#clearUploads()
|
2020-09-16 20:07:28 +08:00
|
|
|
})
|
2023-10-30 16:59:54 +08:00
|
|
|
.on('data', buffer => this.#write(buffer))
|
2023-10-26 19:02:46 +08:00
|
|
|
.on('end', () => {
|
2023-10-27 19:16:32 +08:00
|
|
|
if (this.#error) {
|
2023-10-25 18:45:16 +08:00
|
|
|
return
|
|
|
|
}
|
2023-10-27 19:16:32 +08:00
|
|
|
let err = this.#parser.end()
|
2023-10-25 18:45:16 +08:00
|
|
|
if (err) {
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#handleError(err)
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2020-09-16 20:07:28 +08:00
|
|
|
})
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:59:54 +08:00
|
|
|
#write(buffer) {
|
2023-10-27 19:16:32 +08:00
|
|
|
if (this.#error) {
|
2023-10-25 18:45:16 +08:00
|
|
|
return
|
|
|
|
}
|
2023-10-27 19:16:32 +08:00
|
|
|
if (!this.#parser) {
|
|
|
|
return this.#handleError(new Error('uninitialized parser'))
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
|
|
|
this.bytesReceived += buffer.length
|
|
|
|
this.emit('progress', this.bytesReceived, this.bytesExpected)
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#parser.write(buffer)
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#handlePart(part) {
|
2023-10-25 18:45:16 +08:00
|
|
|
if (part.filename === undefined) {
|
2023-10-30 16:59:54 +08:00
|
|
|
let value = Buffer.from('')
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
part
|
2024-07-10 16:44:17 +08:00
|
|
|
.on('data', buf => {
|
|
|
|
value = Buffer.concat([value, buf])
|
2023-10-27 19:16:32 +08:00
|
|
|
})
|
|
|
|
.on('end', () => {
|
2023-10-30 16:59:54 +08:00
|
|
|
this.emit('field', part.name, value.toString(this.encoding))
|
2023-10-27 19:16:32 +08:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
let file = new File({
|
|
|
|
path: randomPath(this.uploadDir),
|
|
|
|
name: part.filename,
|
|
|
|
type: part.mime
|
2023-10-25 18:45:16 +08:00
|
|
|
})
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
file.open()
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#openedFiles.push(file)
|
2023-10-30 16:41:37 +08:00
|
|
|
// 表单解析完的时候文件写入不一定完成了, 所以需要加入pending计数
|
|
|
|
this.#pending++
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
part
|
|
|
|
.on('data', buffer => {
|
|
|
|
if (buffer.length == 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
file.write(buffer)
|
|
|
|
})
|
|
|
|
.on('end', () => {
|
2023-10-30 16:41:37 +08:00
|
|
|
if (part.ended) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
part.ended = true
|
2023-10-27 19:16:32 +08:00
|
|
|
file.end(() => {
|
|
|
|
this.emit('file', part.name, file)
|
2023-10-30 16:41:37 +08:00
|
|
|
this.#pending--
|
2023-10-27 19:16:32 +08:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
|
|
|
#parseContentType() {
|
2023-10-31 14:28:15 +08:00
|
|
|
let contentType = this.headers['content-type'] || ''
|
2023-10-27 19:16:32 +08:00
|
|
|
let lower = contentType.toLowerCase()
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
if (this.bytesExpected === 0) {
|
|
|
|
return (this.#parser = new EmptyParser())
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
if (lower.includes('octet-stream')) {
|
|
|
|
return this.#createStreamParser()
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
if (lower.includes('urlencoded')) {
|
|
|
|
return this.#createUrlencodedParser()
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
if (lower.includes('multipart')) {
|
|
|
|
let matches = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/)
|
|
|
|
if (matches) {
|
|
|
|
this.#createMultipartParser(matches[1] || matches[2])
|
2023-10-25 18:45:16 +08:00
|
|
|
} else {
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#handleError(new TypeError('unknow multipart boundary'))
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
2023-10-25 18:45:16 +08:00
|
|
|
return
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
if (lower.match(/json|appliation|plain|text/)) {
|
|
|
|
return this.#createJsonParser()
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2024-07-10 16:44:17 +08:00
|
|
|
this.#createBufferParser()
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
|
|
|
#parseContentLength() {
|
2023-10-25 18:45:16 +08:00
|
|
|
this.bytesReceived = 0
|
|
|
|
if (this.headers['content-length']) {
|
2023-10-26 19:02:46 +08:00
|
|
|
this.bytesExpected = +this.headers['content-length']
|
2023-10-25 18:45:16 +08:00
|
|
|
} else if (this.headers['transfer-encoding'] === undefined) {
|
|
|
|
this.bytesExpected = 0
|
|
|
|
}
|
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#createMultipartParser(boundary) {
|
|
|
|
let headerField, headerValue, part
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
this.#parser = new MultipartParser(boundary)
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
this.#parser.$partBegin = function () {
|
2023-10-30 16:59:54 +08:00
|
|
|
part = new EventEmitter()
|
2023-10-30 16:41:37 +08:00
|
|
|
part.readable = true
|
|
|
|
part.headers = {}
|
|
|
|
part.name = null
|
|
|
|
part.filename = null
|
|
|
|
part.mime = null
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
part.transferEncoding = 'binary'
|
|
|
|
part.transferBuffer = ''
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
headerField = ''
|
|
|
|
headerValue = ''
|
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
this.#parser.$headerField = b => {
|
|
|
|
headerField += b.toString(this.encoding)
|
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
this.#parser.$headerValue = b => {
|
|
|
|
headerValue += b.toString(this.encoding)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#parser.$headerEnd = () => {
|
|
|
|
headerField = headerField.toLowerCase()
|
|
|
|
part.headers[headerField] = headerValue
|
|
|
|
|
|
|
|
let matches = headerValue.match(/\bname="([^"]+)"/i)
|
|
|
|
if (headerField == 'content-disposition') {
|
|
|
|
if (matches) {
|
|
|
|
part.name = matches[1]
|
2023-10-27 19:16:32 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
part.filename = parseFilename(headerValue)
|
|
|
|
} else if (headerField == 'content-type') {
|
|
|
|
part.mime = headerValue
|
|
|
|
} else if (headerField == 'content-transfer-encoding') {
|
|
|
|
part.transferEncoding = headerValue.toLowerCase()
|
|
|
|
}
|
|
|
|
|
|
|
|
headerField = ''
|
|
|
|
headerValue = ''
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#parser.$headersEnd = () => {
|
|
|
|
switch (part.transferEncoding) {
|
|
|
|
case 'binary':
|
|
|
|
case '7bit':
|
|
|
|
case '8bit':
|
|
|
|
this.#parser.$partData = function (b) {
|
|
|
|
part.emit('data', b)
|
|
|
|
}
|
|
|
|
this.#parser.$partEnd = function () {
|
|
|
|
part.emit('end')
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case 'base64':
|
|
|
|
this.#parser.$partData = function (b) {
|
|
|
|
part.transferBuffer += b.toString('ascii')
|
|
|
|
|
|
|
|
// 确保offset的值能被4整除
|
|
|
|
let offset = ~~(part.transferBuffer.length / 4) * 4
|
|
|
|
part.emit(
|
|
|
|
'data',
|
|
|
|
Buffer.from(part.transferBuffer.slice(0, offset), 'base64')
|
|
|
|
)
|
|
|
|
part.transferBuffer = part.transferBuffer.slice(offset)
|
|
|
|
}
|
|
|
|
this.#parser.$partEnd = function () {
|
|
|
|
part.emit('data', Buffer.from(part.transferBuffer, 'base64'))
|
|
|
|
part.emit('end')
|
|
|
|
}
|
|
|
|
break
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
default:
|
|
|
|
return this.#handleError(new Error('unknown transfer-encoding'))
|
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-30 16:41:37 +08:00
|
|
|
this.#handlePart(part)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#parser.$end = () => {
|
|
|
|
if (this.#pending > 0) {
|
|
|
|
setTimeout(_ => this.#parser.$end())
|
|
|
|
} else {
|
|
|
|
this.#handleEnd()
|
|
|
|
}
|
|
|
|
}
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#createUrlencodedParser() {
|
|
|
|
this.#parser = new UrlencodedParser()
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#parser
|
|
|
|
.on('field', fields => this.emit('field', false, fields))
|
|
|
|
.on('end', () => this.#handleEnd())
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#createStreamParser() {
|
|
|
|
let filename = this.headers['x-file-name']
|
|
|
|
let mime = this.headers['x-file-type']
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#parser = new OctetParser(filename, mime, randomPath(this.uploadDir))
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
if (this.bytesExpected) {
|
|
|
|
this.#parser.initLength(this.bytesExpected)
|
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#parser
|
|
|
|
.on('file', file => {
|
|
|
|
this.emit('file', false, file)
|
2023-10-25 18:45:16 +08:00
|
|
|
})
|
2023-10-27 19:16:32 +08:00
|
|
|
.on('end', () => this.#handleEnd())
|
|
|
|
.on('error', err => this.#handleError(err))
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#createJsonParser() {
|
|
|
|
this.#parser = new JSONParser()
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-25 18:45:16 +08:00
|
|
|
if (this.bytesExpected) {
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#parser.initLength(this.bytesExpected)
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#parser
|
|
|
|
.on('field', (key, val) => {
|
|
|
|
this.emit('field', key, val)
|
|
|
|
})
|
|
|
|
.on('end', () => this.#handleEnd())
|
|
|
|
.on('error', err => this.#handleError(err))
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2024-07-10 16:44:17 +08:00
|
|
|
#createBufferParser() {
|
|
|
|
this.#parser = new BufferParser()
|
|
|
|
|
|
|
|
if (this.bytesExpected) {
|
|
|
|
this.#parser.initLength(this.bytesExpected)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#parser
|
|
|
|
.on('buffer', buf => {
|
|
|
|
this.emit('buffer', buf)
|
|
|
|
})
|
|
|
|
.on('end', () => this.#handleEnd())
|
|
|
|
.on('error', err => this.#handleError(err))
|
|
|
|
}
|
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#clearUploads() {
|
|
|
|
while (this.#openedFiles.length) {
|
|
|
|
let file = this.#openedFiles.pop()
|
|
|
|
file._writeStream.destroy()
|
|
|
|
setTimeout(_ => {
|
|
|
|
try {
|
|
|
|
fs.unlink(file.path)
|
|
|
|
} catch (e) {}
|
|
|
|
})
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-27 19:16:32 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#handleError(err) {
|
|
|
|
if (this.#error || this.#ended) {
|
|
|
|
return
|
2023-10-25 18:45:16 +08:00
|
|
|
}
|
2023-10-27 19:16:32 +08:00
|
|
|
this.error = true
|
|
|
|
this.emit('error', err)
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
2023-10-26 19:02:46 +08:00
|
|
|
|
2023-10-27 19:16:32 +08:00
|
|
|
#handleEnd() {
|
|
|
|
if (this.#ended || this.#error) {
|
2023-10-25 18:45:16 +08:00
|
|
|
return
|
|
|
|
}
|
2023-10-27 19:16:32 +08:00
|
|
|
this.#ended = true
|
2023-10-25 18:45:16 +08:00
|
|
|
this.emit('end')
|
2020-09-16 20:07:28 +08:00
|
|
|
}
|
|
|
|
}
|