still incomplete and probably bad

This commit is contained in:
May 2024-03-03 23:34:14 -08:00
parent 971ee31d73
commit 5d3fb29c1f
3 changed files with 76 additions and 13 deletions

View file

@ -256,7 +256,7 @@ export class UploadStream extends Writable {
setUploadId(id: string) { setUploadId(id: string) {
if (this.uploadId) if (this.uploadId)
return this.destroy( new WebError(400, "duplicate attempt to set upload ID") ) return this.destroy( new WebError(400, "duplicate attempt to set upload ID") )
if (id.match(id_check_regex)?.[0] != id if (!id || id.match(id_check_regex)?.[0] != id
|| id.length > this.files.config.maxUploadIdLength) || id.length > this.files.config.maxUploadIdLength)
return this.destroy( new WebError(400, "invalid file ID") ) return this.destroy( new WebError(400, "invalid file ID") )

View file

@ -1,14 +1,27 @@
import { Transform, Duplex } from "node:stream"; import { Transform, Readable } from "node:stream";
import { TransformCallback } from "stream"; import { TransformCallback } from "stream";
let content_disposition_matcher = /\s*([^=;]+)(?:=(?:"((?:\\"|[^"])*)"|([^;]*))?;?|;?)/g // probably a bad regex but IDC let content_disposition_matcher = /\s*([^=;]+)(?:=(?:"((?:\\"|[^"])*)"|([^;]*))?;?|;?)/g // probably a bad regex but IDC
/**
* @description Checks if a chunk can be completed by something else (ex. a boundary)
* @param chunk Chunk to perform check on
* @param cmp Chunk to check whether or not something is completable with
* @returns Whether or not this chunk could be completed by cmp
*/
function endChk(chunk: Buffer, cmp: Buffer) {
for (let i = cmp.byteLength-1; i > 0; i--)
if (chunk.subarray(-(i-1)).equals(cmp.subarray(0,i)))
return true
return false
}
export type Headers = { export type Headers = {
["content-disposition"]?: (string|{key: string, value: string})[], ["content-disposition"]?: Record<string, boolean|string>,
["content-type"]?: string ["content-type"]?: string
} }
export class Field extends Duplex { export class Field extends Readable {
headers: Headers = {} headers: Headers = {}
@ -20,9 +33,28 @@ export class Field extends Duplex {
) )
if (this.headers["content-disposition"]) if (this.headers["content-disposition"])
this.headers["content-disposition"] = Array.from( this.headers["content-disposition"] = Object.fromEntries(Array.from(
(this.headers["content-disposition"] as unknown as string) (this.headers["content-disposition"] as unknown as string)
.matchAll(content_disposition_matcher)).map(e => e[2] ? {key: e[1], value: e[2]} : e[1]) .matchAll(content_disposition_matcher)).map(e => [e[1], e[2] ? e[2] : true]))
}
_read(size: number): void {
this.emit("hungry")
}
collect(maxSize: number = 0) {
return new Promise<Buffer>((res,rej) => {
let bufs: Buffer[] = []
this.on("data", (data) => {
if (maxSize && bufs.reduce((cur, acc) => cur+acc.byteLength, 0) > maxSize)
this.destroy(new Error("went above collect()'s maxSize"))
bufs.push(data)
})
this.on("end", () => res(Buffer.concat(bufs)))
this.on("error", (err) => rej(err))
})
} }
} }
@ -31,15 +63,18 @@ export default class FormDataParser extends Transform {
readableObjectMode = true readableObjectMode = true
boundary: string readonly boundary: string
internalBuffer: Buffer | undefined private workingMemory: Buffer | undefined
private workingField: Field | undefined
constructor(boundary: string) { constructor(boundary: string) {
super() super()
this.boundary = boundary this.boundary = boundary
} }
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void { _transform(_chunk: any, encoding: BufferEncoding, callback: TransformCallback): void {
let chunk = this.workingMemory ? Buffer.concat([this.workingMemory, _chunk]) : _chunk
} }

View file

@ -7,7 +7,9 @@ import RangeParser, { type Range } from "range-parser"
import ServeError from "../../../lib/errors.js" import ServeError from "../../../lib/errors.js"
import Files, { WebError } from "../../../lib/files.js" import Files, { WebError } from "../../../lib/files.js"
import { getAccount, requiresPermissions } from "../../../lib/middleware.js" import { getAccount, requiresPermissions } from "../../../lib/middleware.js"
import FormDataParser, { Field } from "../../../lib/formdata.js"
import {Readable} from "node:stream" import {Readable} from "node:stream"
import {ReadableStream as StreamWebReadable} from "node:stream/web"
export let primaryApi = new Hono<{ export let primaryApi = new Hono<{
Variables: { Variables: {
account: Accounts.Account account: Accounts.Account
@ -95,19 +97,45 @@ export default function (files: Files) {
primaryApi.post( primaryApi.post(
"/upload", "/upload",
requiresPermissions("upload"), requiresPermissions("upload"),
async (ctx) => { (ctx) => { return new Promise((resolve,reject) => {
let acc = ctx.get("account") as Accounts.Account let acc = ctx.get("account") as Accounts.Account
if (!ctx.req.header("Content-Type")?.startsWith("multipart/form-data")) { if (!ctx.req.header("Content-Type")?.startsWith("multipart/form-data")) {
ctx.status(400) ctx.status(400)
return ctx.body("[err] must be multipart/form-data") resolve(ctx.body("[err] must be multipart/form-data"))
} }
if (!ctx.req.raw.body) { if (!ctx.req.raw.body) {
ctx.status(400) ctx.status(400)
return ctx.body("[err] body must be supplied") resolve(ctx.body("[err] body must be supplied"))
}
let file = files.createWriteStream(acc.id)
let formDataParser = new FormDataParser('')
Readable.fromWeb(ctx.req.raw.body as StreamWebReadable)
.pipe(formDataParser)
.on("data", async (field: Field) => {
if (field.headers["content-disposition"]?.filename) {
field.pipe(file)
} else {
switch(field.headers["content-disposition"]?.name) {
case "uploadId":
file.setUploadId((await field.collect(65536).catch(e => {formDataParser.destroy(new WebError(413, e.message))}))?.toString() || "")
} }
} }
})
.on("end", async () => {
if (!file.writableEnded) await new Promise((res, rej) => {file.once("finish", res); file.once("error", res)})
if (file.errored || !(await file.commit().catch(e => file.error = e))) {
ctx.status(file.error instanceof WebError ? file.error.statusCode : 500)
resolve(`[err] ${file.error instanceof WebError ? file.error.message : file.error?.toString()}`)
return
}
resolve(ctx.body(file.uploadId!))
})
})}
) )
/* /*
primaryApi.post( primaryApi.post(