summaryrefslogtreecommitdiff
path: root/multipart/multipart.ts
diff options
context:
space:
mode:
authorYusuke Sakurai <kerokerokerop@gmail.com>2019-02-11 08:49:48 +0900
committerRyan Dahl <ry@tinyclouds.org>2019-02-10 18:49:48 -0500
commit33f62789cde407059abba0a7ac18b2145c648ea7 (patch)
tree454d487f232a61f6c57e2ebdad66e49bf939e4d6 /multipart/multipart.ts
parented20bda6ec324b8143c6210024647d2692232c26 (diff)
feat: multipart, etc.. (denoland/deno_std#180)
Original: https://github.com/denoland/deno_std/commit/fda9c98d055091fa886fa444ebd1adcd2ecd21bc
Diffstat (limited to 'multipart/multipart.ts')
-rw-r--r--multipart/multipart.ts492
1 files changed, 492 insertions, 0 deletions
diff --git a/multipart/multipart.ts b/multipart/multipart.ts
new file mode 100644
index 000000000..f0caa2160
--- /dev/null
+++ b/multipart/multipart.ts
@@ -0,0 +1,492 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+import { Buffer, Closer, copy, Reader, ReadResult, remove, Writer } from "deno";
+
+import { FormFile } from "./formfile.ts";
+import {
+ bytesFindIndex,
+ bytesFindLastIndex,
+ bytesHasPrefix,
+ bytesEqual
+} from "../bytes/bytes.ts";
+import { copyN } from "../io/ioutil.ts";
+import { MultiReader } from "../io/readers.ts";
+import { tempFile } from "../io/util.ts";
+import { BufReader, BufState, BufWriter } from "../io/bufio.ts";
+import { TextProtoReader } from "../textproto/mod.ts";
+import { encoder } from "../strings/strings.ts";
+import * as path from "../fs/path.ts";
+
+function randomBoundary() {
+ let boundary = "--------------------------";
+ for (let i = 0; i < 24; i++) {
+ boundary += Math.floor(Math.random() * 10).toString(16);
+ }
+ return boundary;
+}
+
+/** 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(private reader: 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 */
+ async readForm(
+ maxMemory: number
+ ): Promise<{ [key: string]: string | FormFile }> {
+ const result = Object.create(null);
+ let maxValueBytes = maxMemory + (10 << 20);
+ const buf = new Buffer(new Uint8Array(maxValueBytes));
+ for (;;) {
+ const p = await this.nextPart();
+ if (!p) {
+ break;
+ }
+ if (p.formName === "") {
+ continue;
+ }
+ buf.reset();
+ if (!p.fileName) {
+ // value
+ const n = await copyN(buf, p, maxValueBytes);
+ maxValueBytes -= n;
+ if (maxValueBytes < 0) {
+ throw new RangeError("message too large");
+ }
+ const value = buf.toString();
+ result[p.formName] = value;
+ continue;
+ }
+ // file
+ let formFile: FormFile;
+ const n = await copy(buf, p);
+ if (n > maxMemory) {
+ // too big, write to disk and flush buffer
+ const ext = path.extname(p.fileName);
+ const { file, filepath } = await tempFile(".", {
+ prefix: "multipart-",
+ postfix: ext
+ });
+ try {
+ const size = await copyN(
+ file,
+ new MultiReader(buf, p),
+ maxValueBytes
+ );
+ file.close();
+ formFile = {
+ filename: p.fileName,
+ type: p.headers.get("content-type"),
+ tempfile: filepath,
+ size
+ };
+ } catch (e) {
+ await remove(filepath);
+ }
+ } else {
+ formFile = {
+ filename: p.fileName,
+ type: p.headers.get("content-type"),
+ content: buf.bytes(),
+ size: buf.bytes().byteLength
+ };
+ maxMemory -= n;
+ maxValueBytes -= n;
+ }
+ result[p.formName] = formFile;
+ }
+ return result;
+ }
+
+ private currentPart: PartReader;
+ private partsRead: number;
+
+ private async nextPart(): Promise<PartReader> {
+ if (this.currentPart) {
+ this.currentPart.close();
+ }
+ if (bytesEqual(this.dashBoundary, encoder.encode("--"))) {
+ throw new Error("boundary is empty");
+ }
+ let expectNewPart = false;
+ for (;;) {
+ const [line, state] = await this.bufReader.readSlice("\n".charCodeAt(0));
+ if (state === "EOF" && this.isFinalBoundary(line)) {
+ break;
+ }
+ if (state) {
+ throw new Error("aa" + state.toString());
+ }
+ if (this.isBoundaryDelimiterLine(line)) {
+ this.partsRead++;
+ const r = new TextProtoReader(this.bufReader);
+ const [headers, state] = await r.readMIMEHeader();
+ if (state) {
+ throw state;
+ }
+ const np = new PartReader(this, headers);
+ this.currentPart = np;
+ return np;
+ }
+ if (this.isFinalBoundary(line)) {
+ break;
+ }
+ if (expectNewPart) {
+ throw new Error(`expecting a new Part; got line ${line}`);
+ }
+ if (this.partsRead === 0) {
+ continue;
+ }
+ if (bytesEqual(line, this.newLine)) {
+ expectNewPart = true;
+ continue;
+ }
+ throw new Error(`unexpected line in next(): ${line}`);
+ }
+ }
+
+ private isFinalBoundary(line: Uint8Array) {
+ if (!bytesHasPrefix(line, this.dashBoundaryDash)) {
+ return false;
+ }
+ let rest = line.slice(this.dashBoundaryDash.length, line.length);
+ return rest.length === 0 || bytesEqual(skipLWSPChar(rest), this.newLine);
+ }
+
+ private isBoundaryDelimiterLine(line: Uint8Array) {
+ if (!bytesHasPrefix(line, this.dashBoundary)) {
+ return false;
+ }
+ const rest = line.slice(this.dashBoundary.length);
+ return bytesEqual(skipLWSPChar(rest), this.newLine);
+ }
+}
+
+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);
+}
+
+let i = 0;
+
+class PartReader implements Reader, Closer {
+ n: number = 0;
+ total: number = 0;
+ bufState: BufState = null;
+ index = i++;
+
+ constructor(private mr: MultipartReader, public readonly headers: Headers) {}
+
+ async read(p: Uint8Array): Promise<ReadResult> {
+ const br = this.mr.bufReader;
+ const returnResult = (nread: number, bufState: BufState): ReadResult => {
+ if (bufState && bufState !== "EOF") {
+ throw bufState;
+ }
+ return { nread, eof: bufState === "EOF" };
+ };
+ if (this.n === 0 && !this.bufState) {
+ const [peek] = await br.peek(br.buffered());
+ const [n, state] = scanUntilBoundary(
+ peek,
+ this.mr.dashBoundary,
+ this.mr.newLineDashBoundary,
+ this.total,
+ this.bufState
+ );
+ this.n = n;
+ this.bufState = state;
+ if (this.n === 0 && !this.bufState) {
+ const [_, state] = await br.peek(peek.length + 1);
+ this.bufState = state;
+ if (this.bufState === "EOF") {
+ this.bufState = new RangeError("unexpected eof");
+ }
+ }
+ }
+ if (this.n === 0) {
+ return returnResult(0, this.bufState);
+ }
+
+ let n = 0;
+ if (p.byteLength > this.n) {
+ n = this.n;
+ }
+ const buf = p.slice(0, n);
+ const [nread] = await this.mr.bufReader.readFull(buf);
+ p.set(buf);
+ this.total += nread;
+ this.n -= nread;
+ if (this.n === 0) {
+ return returnResult(n, this.bufState);
+ }
+ return returnResult(n, null);
+ }
+
+ close(): void {}
+
+ private contentDisposition: string;
+ private contentDispositionParams: { [key: string]: string };
+
+ private getContentDispositionParams() {
+ if (this.contentDispositionParams) return this.contentDispositionParams;
+ const cd = this.headers.get("content-disposition");
+ const params = {};
+ const comps = cd.split(";");
+ this.contentDisposition = comps[0];
+ comps
+ .slice(1)
+ .map(v => v.trim())
+ .map(kv => {
+ 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 "";
+ }
+}
+
+export function scanUntilBoundary(
+ buf: Uint8Array,
+ dashBoundary: Uint8Array,
+ newLineDashBoundary: Uint8Array,
+ total: number,
+ state: BufState
+): [number, BufState] {
+ if (total === 0) {
+ if (bytesHasPrefix(buf, dashBoundary)) {
+ switch (matchAfterPrefix(buf, dashBoundary, state)) {
+ case -1:
+ return [dashBoundary.length, null];
+ case 0:
+ return [0, null];
+ case 1:
+ return [0, "EOF"];
+ }
+ if (bytesHasPrefix(dashBoundary, buf)) {
+ return [0, state];
+ }
+ }
+ }
+ const i = bytesFindIndex(buf, newLineDashBoundary);
+ if (i >= 0) {
+ switch (matchAfterPrefix(buf.slice(i), newLineDashBoundary, state)) {
+ case -1:
+ return [i + newLineDashBoundary.length, null];
+ case 0:
+ return [i, null];
+ case 1:
+ return [i, "EOF"];
+ }
+ }
+ if (bytesHasPrefix(newLineDashBoundary, buf)) {
+ return [0, state];
+ }
+ const j = bytesFindLastIndex(buf, newLineDashBoundary.slice(0, 1));
+ if (j >= 0 && bytesHasPrefix(newLineDashBoundary, buf.slice(j))) {
+ return [j, null];
+ }
+ return [buf.length, state];
+}
+
+export function matchAfterPrefix(
+ a: Uint8Array,
+ prefix: Uint8Array,
+ bufState: BufState
+): number {
+ if (a.length === prefix.length) {
+ if (bufState) {
+ return 1;
+ }
+ return 0;
+ }
+ const c = a[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;
+}
+
+class PartWriter implements Writer {
+ closed = false;
+ private readonly partHeader: string;
+ private headersWritten: boolean = false;
+
+ constructor(
+ private writer: 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) {
+ 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() {
+ return this._boundary;
+ }
+
+ private lastPart: PartWriter;
+ private bufWriter: BufWriter;
+ private isClosed: boolean = false;
+
+ constructor(private readonly writer: 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): 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): 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): 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) {
+ const f = await this.createFormField(field);
+ await f.write(encoder.encode(value));
+ }
+
+ async writeFile(field: string, filename: string, file: Reader) {
+ const f = await this.createFormFile(field, filename);
+ await copy(f, file);
+ }
+
+ private flush(): Promise<BufState> {
+ return this.bufWriter.flush();
+ }
+
+ /** Close writer. No additional data can be writen to stream */
+ async close() {
+ 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;
+ }
+}