Skip to main content
Module

std/mime/multipart.ts

Deno standard library
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.import { equals, indexOf, lastIndexOf, startsWith } from "../bytes/mod.ts";import { copyN } from "../io/ioutil.ts";import { MultiReader } from "../io/readers.ts";import { extname } from "../path/mod.ts";import { BufReader, BufWriter } from "../io/bufio.ts";import { encoder } from "../encoding/utf8.ts";import { assert } from "../_util/assert.ts";import { TextProtoReader } from "../textproto/mod.ts";import { hasOwnProperty } from "../_util/has_own_property.ts";
/** FormFile object */export interface FormFile { /** filename */ filename: string; /** content-type header value of file */ type: string; /** byte size of file */ size: number; /** in-memory content of file. Either content or tempfile is set */ content?: Uint8Array; /** temporal file path. * Set if file size is bigger than specified max-memory size at reading form * */ tempfile?: string;}
/** Type guard for FormFile */// deno-lint-ignore no-explicit-anyexport function isFormFile(x: any): x is FormFile { return hasOwnProperty(x, "filename") && hasOwnProperty(x, "type");}
function randomBoundary(): string { let boundary = "--------------------------"; for (let i = 0; i < 24; i++) { boundary += Math.floor(Math.random() * 16).toString(16); } return boundary;}
/** * Checks whether `buf` should be considered to match the boundary. * * The prefix is "--boundary" or "\r\n--boundary" or "\n--boundary", and the * caller has verified already that `hasPrefix(buf, prefix)` is true. * * `matchAfterPrefix()` returns `1` if the buffer does match the boundary, * meaning the prefix is followed by a dash, space, tab, cr, nl, or EOF. * * It returns `-1` if the buffer definitely does NOT match the boundary, * meaning the prefix is followed by some other character. * For example, "--foobar" does not match "--foo". * * It returns `0` more input needs to be read to make the decision, * meaning that `buf.length` and `prefix.length` are the same. */export function matchAfterPrefix( buf: Uint8Array, prefix: Uint8Array, eof: boolean,): -1 | 0 | 1 { if (buf.length === prefix.length) { return eof ? 1 : 0; } const c = buf[prefix.length]; if ( c === " ".charCodeAt(0) || c === "\t".charCodeAt(0) || c === "\r".charCodeAt(0) || c === "\n".charCodeAt(0) || c === "-".charCodeAt(0) ) { return 1; } return -1;}
/** * Scans `buf` to identify how much of it can be safely returned as part of the * `PartReader` body. * * @param buf - The buffer to search for boundaries. * @param dashBoundary - Is "--boundary". * @param newLineDashBoundary - Is "\r\n--boundary" or "\n--boundary", depending * on what mode we are in. The comments below (and the name) assume * "\n--boundary", but either is accepted. * @param total - The number of bytes read out so far. If total == 0, then a * leading "--boundary" is recognized. * @param eof - Whether `buf` contains the final bytes in the stream before EOF. * If `eof` is false, more bytes are expected to follow. * @returns The number of data bytes from buf that can be returned as part of * the `PartReader` body. */export function scanUntilBoundary( buf: Uint8Array, dashBoundary: Uint8Array, newLineDashBoundary: Uint8Array, total: number, eof: boolean,): number | null { if (total === 0) { // At beginning of body, allow dashBoundary. if (startsWith(buf, dashBoundary)) { switch (matchAfterPrefix(buf, dashBoundary, eof)) { case -1: return dashBoundary.length; case 0: return 0; case 1: return null; } } if (startsWith(dashBoundary, buf)) { return 0; } }
// Search for "\n--boundary". const i = indexOf(buf, newLineDashBoundary); if (i >= 0) { switch (matchAfterPrefix(buf.slice(i), newLineDashBoundary, eof)) { case -1: return i + newLineDashBoundary.length; case 0: return i; case 1: return i > 0 ? i : null; } } if (startsWith(newLineDashBoundary, buf)) { return 0; }
// Otherwise, anything up to the final \n is not part of the boundary and so // must be part of the body. Also, if the section from the final \n onward is // not a prefix of the boundary, it too must be part of the body. const j = lastIndexOf(buf, newLineDashBoundary.slice(0, 1)); if (j >= 0 && startsWith(newLineDashBoundary, buf.slice(j))) { return j; }
return buf.length;}
class PartReader implements Deno.Reader, Deno.Closer { n: number | null = 0; total = 0;
constructor(private mr: MultipartReader, public readonly headers: Headers) {}
async read(p: Uint8Array): Promise<number | null> { const br = this.mr.bufReader;
// Read into buffer until we identify some data to return, // or we find a reason to stop (boundary or EOF). let peekLength = 1; while (this.n === 0) { peekLength = Math.max(peekLength, br.buffered()); const peekBuf = await br.peek(peekLength); if (peekBuf === null) { throw new Deno.errors.UnexpectedEof(); } const eof = peekBuf.length < peekLength; this.n = scanUntilBoundary( peekBuf, this.mr.dashBoundary, this.mr.newLineDashBoundary, this.total, eof, ); if (this.n === 0) { // Force buffered I/O to read more into buffer. assert(eof === false); peekLength++; } }
if (this.n === null) { return null; }
const nread = Math.min(p.length, this.n); const buf = p.subarray(0, nread); const r = await br.readFull(buf); assert(r === buf); this.n -= nread; this.total += nread; return nread; }
close(): void {}
private contentDisposition!: string; private contentDispositionParams!: { [key: string]: string };
private getContentDispositionParams(): { [key: string]: string } { if (this.contentDispositionParams) return this.contentDispositionParams; const cd = this.headers.get("content-disposition"); const params: { [key: string]: string } = {}; assert(cd != null, "content-disposition must be set"); const comps = decodeURI(cd).split(";"); this.contentDisposition = comps[0]; comps .slice(1) .map((v: string): string => v.trim()) .map((kv: string): void => { const [k, v] = kv.split("="); if (v) { const s = v.charAt(0); const e = v.charAt(v.length - 1); if ((s === e && s === '"') || s === "'") { params[k] = v.substr(1, v.length - 2); } else { params[k] = v; } } }); return (this.contentDispositionParams = params); }
get fileName(): string { return this.getContentDispositionParams()["filename"]; }
get formName(): string { const p = this.getContentDispositionParams(); if (this.contentDisposition === "form-data") { return p["name"]; } return ""; }}
function skipLWSPChar(u: Uint8Array): Uint8Array { const ret = new Uint8Array(u.length); const sp = " ".charCodeAt(0); const ht = "\t".charCodeAt(0); let j = 0; for (let i = 0; i < u.length; i++) { if (u[i] === sp || u[i] === ht) continue; ret[j++] = u[i]; } return ret.slice(0, j);}
export interface MultipartFormData { file(key: string): FormFile | FormFile[] | undefined; value(key: string): string | undefined; entries(): IterableIterator< [string, string | FormFile | FormFile[] | undefined] >; [Symbol.iterator](): IterableIterator< [string, string | FormFile | FormFile[] | undefined] >; /** Remove all tempfiles */ removeAll(): Promise<void>;}
/** Reader for parsing multipart/form-data */export class MultipartReader { readonly newLine = encoder.encode("\r\n"); readonly newLineDashBoundary = encoder.encode(`\r\n--${this.boundary}`); readonly dashBoundaryDash = encoder.encode(`--${this.boundary}--`); readonly dashBoundary = encoder.encode(`--${this.boundary}`); readonly bufReader: BufReader;
constructor(reader: Deno.Reader, private boundary: string) { this.bufReader = new BufReader(reader); }
/** Read all form data from stream. * If total size of stored data in memory exceed maxMemory, * overflowed file data will be written to temporal files. * String field values are never written to files. * null value means parsing or writing to file was failed in some reason. * @param maxMemory maximum memory size to store file in memory. bytes. @default 10485760 (10MB) * */ async readForm(maxMemory = 10 << 20): Promise<MultipartFormData> { const fileMap = new Map<string, FormFile | FormFile[]>(); const valueMap = new Map<string, string>(); let maxValueBytes = maxMemory + (10 << 20); const buf = new Deno.Buffer(new Uint8Array(maxValueBytes)); for (;;) { const p = await this.nextPart(); if (p === null) { break; } if (p.formName === "") { continue; } buf.reset(); if (!p.fileName) { // value const n = await copyN(p, buf, maxValueBytes); maxValueBytes -= n; if (maxValueBytes < 0) { throw new RangeError("message too large"); } const value = new TextDecoder().decode(buf.bytes()); valueMap.set(p.formName, value); continue; } // file let formFile: FormFile | FormFile[] | undefined; const n = await copyN(p, buf, maxValueBytes); const contentType = p.headers.get("content-type"); assert(contentType != null, "content-type must be set"); if (n > maxMemory) { // too big, write to disk and flush buffer const ext = extname(p.fileName); const filepath = await Deno.makeTempFile({ dir: ".", prefix: "multipart-", suffix: ext, });
const file = await Deno.open(filepath, { write: true });
try { const size = await Deno.copy(new MultiReader(buf, p), file);
file.close(); formFile = { filename: p.fileName, type: contentType, tempfile: filepath, size, }; } catch (e) { await Deno.remove(filepath); throw e; } } else { formFile = { filename: p.fileName, type: contentType, content: buf.bytes(), size: buf.length, }; maxMemory -= n; maxValueBytes -= n; } if (formFile) { const mapVal = fileMap.get(p.formName); if (mapVal !== undefined) { if (Array.isArray(mapVal)) { mapVal.push(formFile); } else { fileMap.set(p.formName, [mapVal, formFile]); } } else { fileMap.set(p.formName, formFile); } } } return multipatFormData(fileMap, valueMap); }
private currentPart: PartReader | undefined; private partsRead = 0;
private async nextPart(): Promise<PartReader | null> { if (this.currentPart) { this.currentPart.close(); } if (equals(this.dashBoundary, encoder.encode("--"))) { throw new Error("boundary is empty"); } let expectNewPart = false; for (;;) { const line = await this.bufReader.readSlice("\n".charCodeAt(0)); if (line === null) { throw new Deno.errors.UnexpectedEof(); } if (this.isBoundaryDelimiterLine(line)) { this.partsRead++; const r = new TextProtoReader(this.bufReader); const headers = await r.readMIMEHeader(); if (headers === null) { throw new Deno.errors.UnexpectedEof(); } const np = new PartReader(this, headers); this.currentPart = np; return np; } if (this.isFinalBoundary(line)) { return null; } if (expectNewPart) { throw new Error(`expecting a new Part; got line ${line}`); } if (this.partsRead === 0) { continue; } if (equals(line, this.newLine)) { expectNewPart = true; continue; } throw new Error(`unexpected line in nextPart(): ${line}`); } }
private isFinalBoundary(line: Uint8Array): boolean { if (!startsWith(line, this.dashBoundaryDash)) { return false; } const rest = line.slice(this.dashBoundaryDash.length, line.length); return rest.length === 0 || equals(skipLWSPChar(rest), this.newLine); }
private isBoundaryDelimiterLine(line: Uint8Array): boolean { if (!startsWith(line, this.dashBoundary)) { return false; } const rest = line.slice(this.dashBoundary.length); return equals(skipLWSPChar(rest), this.newLine); }}
function multipatFormData( fileMap: Map<string, FormFile | FormFile[]>, valueMap: Map<string, string>,): MultipartFormData { function file(key: string): FormFile | FormFile[] | undefined { return fileMap.get(key); } function value(key: string): string | undefined { return valueMap.get(key); } function* entries(): IterableIterator< [string, string | FormFile | FormFile[] | undefined] > { yield* fileMap; yield* valueMap; } async function removeAll(): Promise<void> { const promises: Array<Promise<void>> = []; for (const val of fileMap.values()) { if (Array.isArray(val)) { for (const subVal of val) { if (!subVal.tempfile) continue; promises.push(Deno.remove(subVal.tempfile)); } } else { if (!val.tempfile) continue; promises.push(Deno.remove(val.tempfile)); } } await Promise.all(promises); } return { file, value, entries, removeAll, [Symbol.iterator](): IterableIterator< [string, string | FormFile | FormFile[] | undefined] > { return entries(); }, };}
class PartWriter implements Deno.Writer { closed = false; private readonly partHeader: string; private headersWritten = false;
constructor( private writer: Deno.Writer, readonly boundary: string, public headers: Headers, isFirstBoundary: boolean, ) { let buf = ""; if (isFirstBoundary) { buf += `--${boundary}\r\n`; } else { buf += `\r\n--${boundary}\r\n`; } for (const [key, value] of headers.entries()) { buf += `${key}: ${value}\r\n`; } buf += `\r\n`; this.partHeader = buf; }
close(): void { this.closed = true; }
async write(p: Uint8Array): Promise<number> { if (this.closed) { throw new Error("part is closed"); } if (!this.headersWritten) { await this.writer.write(encoder.encode(this.partHeader)); this.headersWritten = true; } return this.writer.write(p); }}
function checkBoundary(b: string): string { if (b.length < 1 || b.length > 70) { throw new Error(`invalid boundary length: ${b.length}`); } const end = b.length - 1; for (let i = 0; i < end; i++) { const c = b.charAt(i); if (!c.match(/[a-zA-Z0-9'()+_,\-./:=?]/) || (c === " " && i !== end)) { throw new Error("invalid boundary character: " + c); } } return b;}
/** Writer for creating multipart/form-data */export class MultipartWriter { private readonly _boundary: string;
get boundary(): string { return this._boundary; }
private lastPart: PartWriter | undefined; private bufWriter: BufWriter; private isClosed = false;
constructor(private readonly writer: Deno.Writer, boundary?: string) { if (boundary !== void 0) { this._boundary = checkBoundary(boundary); } else { this._boundary = randomBoundary(); } this.bufWriter = new BufWriter(writer); }
formDataContentType(): string { return `multipart/form-data; boundary=${this.boundary}`; }
private createPart(headers: Headers): Deno.Writer { if (this.isClosed) { throw new Error("multipart: writer is closed"); } if (this.lastPart) { this.lastPart.close(); } const part = new PartWriter( this.writer, this.boundary, headers, !this.lastPart, ); this.lastPart = part; return part; }
createFormFile(field: string, filename: string): Deno.Writer { const h = new Headers(); h.set( "Content-Disposition", `form-data; name="${field}"; filename="${filename}"`, ); h.set("Content-Type", "application/octet-stream"); return this.createPart(h); }
createFormField(field: string): Deno.Writer { const h = new Headers(); h.set("Content-Disposition", `form-data; name="${field}"`); h.set("Content-Type", "application/octet-stream"); return this.createPart(h); }
async writeField(field: string, value: string): Promise<void> { const f = await this.createFormField(field); await f.write(encoder.encode(value)); }
async writeFile( field: string, filename: string, file: Deno.Reader, ): Promise<void> { const f = await this.createFormFile(field, filename); await Deno.copy(file, f); }
private flush(): Promise<void> { return this.bufWriter.flush(); }
/** Close writer. No additional data can be written to stream */ async close(): Promise<void> { if (this.isClosed) { throw new Error("multipart: writer is closed"); } if (this.lastPart) { this.lastPart.close(); this.lastPart = void 0; } await this.writer.write(encoder.encode(`\r\n--${this.boundary}--\r\n`)); await this.flush(); this.isClosed = true; }}