From f184332c09c851faac50f598d29ebe4426e05464 Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Sat, 9 May 2020 13:34:47 +0100 Subject: BREAKING(std): reorganization (#5087) * Prepend underscores to private modules * Remove collectUint8Arrays() It would be a misuse of Deno.iter()'s result. * Move std/_util/async.ts to std/async * Move std/util/sha*.ts to std/hash --- std/http/_io.ts | 366 +++++++++++++++++++++++++++++++++++ std/http/_io_test.ts | 476 ++++++++++++++++++++++++++++++++++++++++++++++ std/http/_mock_conn.ts | 25 +++ std/http/io.ts | 366 ----------------------------------- std/http/io_test.ts | 476 ---------------------------------------------- std/http/mock.ts | 25 --- std/http/racing_server.ts | 2 +- std/http/server.ts | 4 +- std/http/server_test.ts | 4 +- 9 files changed, 872 insertions(+), 872 deletions(-) create mode 100644 std/http/_io.ts create mode 100644 std/http/_io_test.ts create mode 100644 std/http/_mock_conn.ts delete mode 100644 std/http/io.ts delete mode 100644 std/http/io_test.ts delete mode 100644 std/http/mock.ts (limited to 'std/http') diff --git a/std/http/_io.ts b/std/http/_io.ts new file mode 100644 index 000000000..631adafd0 --- /dev/null +++ b/std/http/_io.ts @@ -0,0 +1,366 @@ +import { BufReader, BufWriter } from "../io/bufio.ts"; +import { TextProtoReader } from "../textproto/mod.ts"; +import { assert } from "../testing/asserts.ts"; +import { encoder } from "../encoding/utf8.ts"; +import { ServerRequest, Response } from "./server.ts"; +import { STATUS_TEXT } from "./http_status.ts"; + +export function emptyReader(): Deno.Reader { + return { + read(_: Uint8Array): Promise { + return Promise.resolve(null); + }, + }; +} + +export function bodyReader(contentLength: number, r: BufReader): Deno.Reader { + let totalRead = 0; + let finished = false; + async function read(buf: Uint8Array): Promise { + if (finished) return null; + let result: number | null; + const remaining = contentLength - totalRead; + if (remaining >= buf.byteLength) { + result = await r.read(buf); + } else { + const readBuf = buf.subarray(0, remaining); + result = await r.read(readBuf); + } + if (result !== null) { + totalRead += result; + } + finished = totalRead === contentLength; + return result; + } + return { read }; +} + +export function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader { + // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 + const tp = new TextProtoReader(r); + let finished = false; + const chunks: Array<{ + offset: number; + data: Uint8Array; + }> = []; + async function read(buf: Uint8Array): Promise { + if (finished) return null; + const [chunk] = chunks; + if (chunk) { + const chunkRemaining = chunk.data.byteLength - chunk.offset; + const readLength = Math.min(chunkRemaining, buf.byteLength); + for (let i = 0; i < readLength; i++) { + buf[i] = chunk.data[chunk.offset + i]; + } + chunk.offset += readLength; + if (chunk.offset === chunk.data.byteLength) { + chunks.shift(); + // Consume \r\n; + if ((await tp.readLine()) === null) { + throw new Deno.errors.UnexpectedEof(); + } + } + return readLength; + } + const line = await tp.readLine(); + if (line === null) throw new Deno.errors.UnexpectedEof(); + // TODO: handle chunk extension + const [chunkSizeString] = line.split(";"); + const chunkSize = parseInt(chunkSizeString, 16); + if (Number.isNaN(chunkSize) || chunkSize < 0) { + throw new Error("Invalid chunk size"); + } + if (chunkSize > 0) { + if (chunkSize > buf.byteLength) { + let eof = await r.readFull(buf); + if (eof === null) { + throw new Deno.errors.UnexpectedEof(); + } + const restChunk = new Uint8Array(chunkSize - buf.byteLength); + eof = await r.readFull(restChunk); + if (eof === null) { + throw new Deno.errors.UnexpectedEof(); + } else { + chunks.push({ + offset: 0, + data: restChunk, + }); + } + return buf.byteLength; + } else { + const bufToFill = buf.subarray(0, chunkSize); + const eof = await r.readFull(bufToFill); + if (eof === null) { + throw new Deno.errors.UnexpectedEof(); + } + // Consume \r\n + if ((await tp.readLine()) === null) { + throw new Deno.errors.UnexpectedEof(); + } + return chunkSize; + } + } else { + assert(chunkSize === 0); + // Consume \r\n + if ((await r.readLine()) === null) { + throw new Deno.errors.UnexpectedEof(); + } + await readTrailers(h, r); + finished = true; + return null; + } + } + return { read }; +} + +const kProhibitedTrailerHeaders = [ + "transfer-encoding", + "content-length", + "trailer", +]; + +/** + * Read trailer headers from reader and append values to headers. + * "trailer" field will be deleted. + * */ +export async function readTrailers( + headers: Headers, + r: BufReader +): Promise { + const keys = parseTrailer(headers.get("trailer")); + if (!keys) return; + const tp = new TextProtoReader(r); + const result = await tp.readMIMEHeader(); + assert(result !== null, "trailer must be set"); + for (const [k, v] of result) { + if (!keys.has(k)) { + throw new Error("Undeclared trailer field"); + } + keys.delete(k); + headers.append(k, v); + } + assert(keys.size === 0, "Missing trailers"); + headers.delete("trailer"); +} + +function parseTrailer(field: string | null): Set | undefined { + if (field == null) { + return undefined; + } + const keys = field.split(",").map((v) => v.trim()); + if (keys.length === 0) { + throw new Error("Empty trailer"); + } + for (const invalid of kProhibitedTrailerHeaders) { + if (keys.includes(invalid)) { + throw new Error(`Prohibited field for trailer`); + } + } + return new Set(keys); +} + +export async function writeChunkedBody( + w: Deno.Writer, + r: Deno.Reader +): Promise { + const writer = BufWriter.create(w); + for await (const chunk of Deno.iter(r)) { + if (chunk.byteLength <= 0) continue; + const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); + const end = encoder.encode("\r\n"); + await writer.write(start); + await writer.write(chunk); + await writer.write(end); + } + + const endChunk = encoder.encode("0\r\n\r\n"); + await writer.write(endChunk); +} + +/** write trailer headers to writer. it mostly should be called after writeResponse */ +export async function writeTrailers( + w: Deno.Writer, + headers: Headers, + trailers: Headers +): Promise { + const trailer = headers.get("trailer"); + if (trailer === null) { + throw new Error('response headers must have "trailer" header field'); + } + const transferEncoding = headers.get("transfer-encoding"); + if (transferEncoding === null || !transferEncoding.match(/^chunked/)) { + throw new Error( + `trailer headers is only allowed for "transfer-encoding: chunked": got "${transferEncoding}"` + ); + } + const writer = BufWriter.create(w); + const trailerHeaderFields = trailer + .split(",") + .map((s) => s.trim().toLowerCase()); + for (const f of trailerHeaderFields) { + assert( + !kProhibitedTrailerHeaders.includes(f), + `"${f}" is prohibited for trailer header` + ); + } + for (const [key, value] of trailers) { + assert( + trailerHeaderFields.includes(key), + `Not trailer header field: ${key}` + ); + await writer.write(encoder.encode(`${key}: ${value}\r\n`)); + } + await writer.write(encoder.encode("\r\n")); + await writer.flush(); +} + +export async function writeResponse( + w: Deno.Writer, + r: Response +): Promise { + const protoMajor = 1; + const protoMinor = 1; + const statusCode = r.status || 200; + const statusText = STATUS_TEXT.get(statusCode); + const writer = BufWriter.create(w); + if (!statusText) { + throw new Deno.errors.InvalidData("Bad status code"); + } + if (!r.body) { + r.body = new Uint8Array(); + } + if (typeof r.body === "string") { + r.body = encoder.encode(r.body); + } + + let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`; + + const headers = r.headers ?? new Headers(); + + if (r.body && !headers.get("content-length")) { + if (r.body instanceof Uint8Array) { + out += `content-length: ${r.body.byteLength}\r\n`; + } else if (!headers.get("transfer-encoding")) { + out += "transfer-encoding: chunked\r\n"; + } + } + + for (const [key, value] of headers) { + out += `${key}: ${value}\r\n`; + } + + out += `\r\n`; + + const header = encoder.encode(out); + const n = await writer.write(header); + assert(n === header.byteLength); + + if (r.body instanceof Uint8Array) { + const n = await writer.write(r.body); + assert(n === r.body.byteLength); + } else if (headers.has("content-length")) { + const contentLength = headers.get("content-length"); + assert(contentLength != null); + const bodyLength = parseInt(contentLength); + const n = await Deno.copy(r.body, writer); + assert(n === bodyLength); + } else { + await writeChunkedBody(writer, r.body); + } + if (r.trailers) { + const t = await r.trailers(); + await writeTrailers(writer, headers, t); + } + await writer.flush(); +} + +/** + * ParseHTTPVersion parses a HTTP version string. + * "HTTP/1.0" returns (1, 0). + * Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request.go#L766-L792 + */ +export function parseHTTPVersion(vers: string): [number, number] { + switch (vers) { + case "HTTP/1.1": + return [1, 1]; + + case "HTTP/1.0": + return [1, 0]; + + default: { + const Big = 1000000; // arbitrary upper bound + + if (!vers.startsWith("HTTP/")) { + break; + } + + const dot = vers.indexOf("."); + if (dot < 0) { + break; + } + + const majorStr = vers.substring(vers.indexOf("/") + 1, dot); + const major = Number(majorStr); + if (!Number.isInteger(major) || major < 0 || major > Big) { + break; + } + + const minorStr = vers.substring(dot + 1); + const minor = Number(minorStr); + if (!Number.isInteger(minor) || minor < 0 || minor > Big) { + break; + } + + return [major, minor]; + } + } + + throw new Error(`malformed HTTP version ${vers}`); +} + +export async function readRequest( + conn: Deno.Conn, + bufr: BufReader +): Promise { + const tp = new TextProtoReader(bufr); + const firstLine = await tp.readLine(); // e.g. GET /index.html HTTP/1.0 + if (firstLine === null) return null; + const headers = await tp.readMIMEHeader(); + if (headers === null) throw new Deno.errors.UnexpectedEof(); + + const req = new ServerRequest(); + req.conn = conn; + req.r = bufr; + [req.method, req.url, req.proto] = firstLine.split(" ", 3); + [req.protoMinor, req.protoMajor] = parseHTTPVersion(req.proto); + req.headers = headers; + fixLength(req); + return req; +} + +function fixLength(req: ServerRequest): void { + const contentLength = req.headers.get("Content-Length"); + if (contentLength) { + const arrClen = contentLength.split(","); + if (arrClen.length > 1) { + const distinct = [...new Set(arrClen.map((e): string => e.trim()))]; + if (distinct.length > 1) { + throw Error("cannot contain multiple Content-Length headers"); + } else { + req.headers.set("Content-Length", distinct[0]); + } + } + const c = req.headers.get("Content-Length"); + if (req.method === "HEAD" && c && c !== "0") { + throw Error("http: method cannot contain a Content-Length"); + } + if (c && req.headers.has("transfer-encoding")) { + // A sender MUST NOT send a Content-Length header field in any message + // that contains a Transfer-Encoding header field. + // rfc: https://tools.ietf.org/html/rfc7230#section-3.3.2 + throw new Error( + "http: Transfer-Encoding and Content-Length cannot be send together" + ); + } + } +} diff --git a/std/http/_io_test.ts b/std/http/_io_test.ts new file mode 100644 index 000000000..c22ebdf07 --- /dev/null +++ b/std/http/_io_test.ts @@ -0,0 +1,476 @@ +import { + AssertionError, + assertThrowsAsync, + assertEquals, + assert, + assertNotEquals, +} from "../testing/asserts.ts"; +import { + bodyReader, + chunkedBodyReader, + writeTrailers, + readTrailers, + parseHTTPVersion, + readRequest, + writeResponse, +} from "./_io.ts"; +import { encode, decode } from "../encoding/utf8.ts"; +import { BufReader, ReadLineResult } from "../io/bufio.ts"; +import { ServerRequest, Response } from "./server.ts"; +import { StringReader } from "../io/readers.ts"; +import { mockConn } from "./_mock_conn.ts"; +const { Buffer, test, readAll } = Deno; + +test("bodyReader", async () => { + const text = "Hello, Deno"; + const r = bodyReader(text.length, new BufReader(new Buffer(encode(text)))); + assertEquals(decode(await Deno.readAll(r)), text); +}); +function chunkify(n: number, char: string): string { + const v = Array.from({ length: n }) + .map(() => `${char}`) + .join(""); + return `${n.toString(16)}\r\n${v}\r\n`; +} +test("chunkedBodyReader", async () => { + const body = [ + chunkify(3, "a"), + chunkify(5, "b"), + chunkify(11, "c"), + chunkify(22, "d"), + chunkify(0, ""), + ].join(""); + const h = new Headers(); + const r = chunkedBodyReader(h, new BufReader(new Buffer(encode(body)))); + let result: number | null; + // Use small buffer as some chunks exceed buffer size + const buf = new Uint8Array(5); + const dest = new Buffer(); + while ((result = await r.read(buf)) !== null) { + const len = Math.min(buf.byteLength, result); + await dest.write(buf.subarray(0, len)); + } + const exp = "aaabbbbbcccccccccccdddddddddddddddddddddd"; + assertEquals(new TextDecoder().decode(dest.bytes()), exp); +}); + +test("chunkedBodyReader with trailers", async () => { + const body = [ + chunkify(3, "a"), + chunkify(5, "b"), + chunkify(11, "c"), + chunkify(22, "d"), + chunkify(0, ""), + "deno: land\r\n", + "node: js\r\n", + "\r\n", + ].join(""); + const h = new Headers({ + trailer: "deno,node", + }); + const r = chunkedBodyReader(h, new BufReader(new Buffer(encode(body)))); + assertEquals(h.has("trailer"), true); + assertEquals(h.has("deno"), false); + assertEquals(h.has("node"), false); + const act = decode(await Deno.readAll(r)); + const exp = "aaabbbbbcccccccccccdddddddddddddddddddddd"; + assertEquals(act, exp); + assertEquals(h.has("trailer"), false); + assertEquals(h.get("deno"), "land"); + assertEquals(h.get("node"), "js"); +}); + +test("readTrailers", async () => { + const h = new Headers({ + trailer: "deno,node", + }); + const trailer = ["deno: land", "node: js", "", ""].join("\r\n"); + await readTrailers(h, new BufReader(new Buffer(encode(trailer)))); + assertEquals(h.has("trailer"), false); + assertEquals(h.get("deno"), "land"); + assertEquals(h.get("node"), "js"); +}); + +test("readTrailer should throw if undeclared headers found in trailer", async () => { + const patterns = [ + ["deno,node", "deno: land\r\nnode: js\r\ngo: lang\r\n\r\n"], + ["deno", "node: js\r\n\r\n"], + ["deno", "node:js\r\ngo: lang\r\n\r\n"], + ]; + for (const [header, trailer] of patterns) { + const h = new Headers({ + trailer: header, + }); + await assertThrowsAsync( + async () => { + await readTrailers(h, new BufReader(new Buffer(encode(trailer)))); + }, + Error, + "Undeclared trailer field" + ); + } +}); + +test("readTrailer should throw if trailer contains prohibited fields", async () => { + for (const f of ["content-length", "trailer", "transfer-encoding"]) { + const h = new Headers({ + trailer: f, + }); + await assertThrowsAsync( + async () => { + await readTrailers(h, new BufReader(new Buffer())); + }, + Error, + "Prohibited field for trailer" + ); + } +}); + +test("writeTrailer", async () => { + const w = new Buffer(); + await writeTrailers( + w, + new Headers({ "transfer-encoding": "chunked", trailer: "deno,node" }), + new Headers({ deno: "land", node: "js" }) + ); + assertEquals( + new TextDecoder().decode(w.bytes()), + "deno: land\r\nnode: js\r\n\r\n" + ); +}); + +test("writeTrailer should throw", async () => { + const w = new Buffer(); + await assertThrowsAsync( + () => { + return writeTrailers(w, new Headers(), new Headers()); + }, + Error, + 'must have "trailer"' + ); + await assertThrowsAsync( + () => { + return writeTrailers(w, new Headers({ trailer: "deno" }), new Headers()); + }, + Error, + "only allowed" + ); + for (const f of ["content-length", "trailer", "transfer-encoding"]) { + await assertThrowsAsync( + () => { + return writeTrailers( + w, + new Headers({ "transfer-encoding": "chunked", trailer: f }), + new Headers({ [f]: "1" }) + ); + }, + AssertionError, + "prohibited" + ); + } + await assertThrowsAsync( + () => { + return writeTrailers( + w, + new Headers({ "transfer-encoding": "chunked", trailer: "deno" }), + new Headers({ node: "js" }) + ); + }, + AssertionError, + "Not trailer" + ); +}); + +// Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request_test.go#L535-L565 +test("parseHttpVersion", (): void => { + const testCases = [ + { in: "HTTP/0.9", want: [0, 9] }, + { in: "HTTP/1.0", want: [1, 0] }, + { in: "HTTP/1.1", want: [1, 1] }, + { in: "HTTP/3.14", want: [3, 14] }, + { in: "HTTP", err: true }, + { in: "HTTP/one.one", err: true }, + { in: "HTTP/1.1/", err: true }, + { in: "HTTP/-1.0", err: true }, + { in: "HTTP/0.-1", err: true }, + { in: "HTTP/", err: true }, + { in: "HTTP/1,0", err: true }, + { in: "HTTP/1.1000001", err: true }, + ]; + for (const t of testCases) { + let r, err; + try { + r = parseHTTPVersion(t.in); + } catch (e) { + err = e; + } + if (t.err) { + assert(err instanceof Error, t.in); + } else { + assertEquals(err, undefined); + assertEquals(r, t.want, t.in); + } + } +}); + +test("writeUint8ArrayResponse", async function (): Promise { + const shortText = "Hello"; + + const body = new TextEncoder().encode(shortText); + const res: Response = { body }; + + const buf = new Deno.Buffer(); + await writeResponse(buf, res); + + const decoder = new TextDecoder("utf-8"); + const reader = new BufReader(buf); + + let r: ReadLineResult | null = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), `content-length: ${shortText.length}`); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(r.line.byteLength, 0); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), shortText); + assertEquals(r.more, false); + + const eof = await reader.readLine(); + assertEquals(eof, null); +}); + +test("writeStringResponse", async function (): Promise { + const body = "Hello"; + + const res: Response = { body }; + + const buf = new Deno.Buffer(); + await writeResponse(buf, res); + + const decoder = new TextDecoder("utf-8"); + const reader = new BufReader(buf); + + let r: ReadLineResult | null = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), `content-length: ${body.length}`); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(r.line.byteLength, 0); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), body); + assertEquals(r.more, false); + + const eof = await reader.readLine(); + assertEquals(eof, null); +}); + +test("writeStringReaderResponse", async function (): Promise { + const shortText = "Hello"; + + const body = new StringReader(shortText); + const res: Response = { body }; + + const buf = new Deno.Buffer(); + await writeResponse(buf, res); + + const decoder = new TextDecoder("utf-8"); + const reader = new BufReader(buf); + + let r: ReadLineResult | null = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), "transfer-encoding: chunked"); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(r.line.byteLength, 0); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), shortText.length.toString()); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), shortText); + assertEquals(r.more, false); + + r = await reader.readLine(); + assert(r !== null); + assertEquals(decoder.decode(r.line), "0"); + assertEquals(r.more, false); +}); + +test("writeResponse with trailer", async () => { + const w = new Buffer(); + const body = new StringReader("Hello"); + await writeResponse(w, { + status: 200, + headers: new Headers({ + "transfer-encoding": "chunked", + trailer: "deno,node", + }), + body, + trailers: () => new Headers({ deno: "land", node: "js" }), + }); + const ret = new TextDecoder().decode(w.bytes()); + const exp = [ + "HTTP/1.1 200 OK", + "transfer-encoding: chunked", + "trailer: deno,node", + "", + "5", + "Hello", + "0", + "", + "deno: land", + "node: js", + "", + "", + ].join("\r\n"); + assertEquals(ret, exp); +}); + +test("writeResponseShouldNotModifyOriginHeaders", async () => { + const headers = new Headers(); + const buf = new Deno.Buffer(); + + await writeResponse(buf, { body: "foo", headers }); + assert(decode(await readAll(buf)).includes("content-length: 3")); + + await writeResponse(buf, { body: "hello", headers }); + assert(decode(await readAll(buf)).includes("content-length: 5")); +}); + +test("readRequestError", async function (): Promise { + const input = `GET / HTTP/1.1 +malformedHeader +`; + const reader = new BufReader(new StringReader(input)); + let err; + try { + await readRequest(mockConn(), reader); + } catch (e) { + err = e; + } + assert(err instanceof Error); + assertEquals(err.message, "malformed MIME header line: malformedHeader"); +}); + +// Ported from Go +// https://github.com/golang/go/blob/go1.12.5/src/net/http/request_test.go#L377-L443 +// TODO(zekth) fix tests +test("testReadRequestError", async function (): Promise { + const testCases = [ + { + in: "GET / HTTP/1.1\r\nheader: foo\r\n\r\n", + headers: [{ key: "header", value: "foo" }], + }, + { + in: "GET / HTTP/1.1\r\nheader:foo\r\n", + err: Deno.errors.UnexpectedEof, + }, + { in: "", eof: true }, + { + in: "HEAD / HTTP/1.1\r\nContent-Length:4\r\n\r\n", + err: "http: method cannot contain a Content-Length", + }, + { + in: "HEAD / HTTP/1.1\r\n\r\n", + headers: [], + }, + // Multiple Content-Length values should either be + // deduplicated if same or reject otherwise + // See Issue 16490. + { + in: + "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 0\r\n\r\n" + + "Gopher hey\r\n", + err: "cannot contain multiple Content-Length headers", + }, + { + in: + "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 6\r\n\r\n" + + "Gopher\r\n", + err: "cannot contain multiple Content-Length headers", + }, + { + in: + "PUT / HTTP/1.1\r\nContent-Length: 6 \r\nContent-Length: 6\r\n" + + "Content-Length:6\r\n\r\nGopher\r\n", + headers: [{ key: "Content-Length", value: "6" }], + }, + { + in: "PUT / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 6 \r\n\r\n", + err: "cannot contain multiple Content-Length headers", + }, + // Setting an empty header is swallowed by textproto + // see: readMIMEHeader() + // { + // in: "POST / HTTP/1.1\r\nContent-Length:\r\nContent-Length: 3\r\n\r\n", + // err: "cannot contain multiple Content-Length headers" + // }, + { + in: "HEAD / HTTP/1.1\r\nContent-Length:0\r\nContent-Length: 0\r\n\r\n", + headers: [{ key: "Content-Length", value: "0" }], + }, + { + in: + "POST / HTTP/1.1\r\nContent-Length:0\r\ntransfer-encoding: " + + "chunked\r\n\r\n", + headers: [], + err: "http: Transfer-Encoding and Content-Length cannot be send together", + }, + ]; + for (const test of testCases) { + const reader = new BufReader(new StringReader(test.in)); + let err; + let req: ServerRequest | null = null; + try { + req = await readRequest(mockConn(), reader); + } catch (e) { + err = e; + } + if (test.eof) { + assertEquals(req, null); + } else if (typeof test.err === "string") { + assertEquals(err.message, test.err); + } else if (test.err) { + assert(err instanceof (test.err as typeof Deno.errors.UnexpectedEof)); + } else { + assert(req instanceof ServerRequest); + assert(test.headers); + assertEquals(err, undefined); + assertNotEquals(req, null); + for (const h of test.headers) { + assertEquals(req.headers.get(h.key), h.value); + } + } + } +}); diff --git a/std/http/_mock_conn.ts b/std/http/_mock_conn.ts new file mode 100644 index 000000000..be07ede24 --- /dev/null +++ b/std/http/_mock_conn.ts @@ -0,0 +1,25 @@ +/** Create dummy Deno.Conn object with given base properties */ +export function mockConn(base: Partial = {}): Deno.Conn { + return { + localAddr: { + transport: "tcp", + hostname: "", + port: 0, + }, + remoteAddr: { + transport: "tcp", + hostname: "", + port: 0, + }, + rid: -1, + closeWrite: (): void => {}, + read: (): Promise => { + return Promise.resolve(0); + }, + write: (): Promise => { + return Promise.resolve(-1); + }, + close: (): void => {}, + ...base, + }; +} diff --git a/std/http/io.ts b/std/http/io.ts deleted file mode 100644 index 631adafd0..000000000 --- a/std/http/io.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { BufReader, BufWriter } from "../io/bufio.ts"; -import { TextProtoReader } from "../textproto/mod.ts"; -import { assert } from "../testing/asserts.ts"; -import { encoder } from "../encoding/utf8.ts"; -import { ServerRequest, Response } from "./server.ts"; -import { STATUS_TEXT } from "./http_status.ts"; - -export function emptyReader(): Deno.Reader { - return { - read(_: Uint8Array): Promise { - return Promise.resolve(null); - }, - }; -} - -export function bodyReader(contentLength: number, r: BufReader): Deno.Reader { - let totalRead = 0; - let finished = false; - async function read(buf: Uint8Array): Promise { - if (finished) return null; - let result: number | null; - const remaining = contentLength - totalRead; - if (remaining >= buf.byteLength) { - result = await r.read(buf); - } else { - const readBuf = buf.subarray(0, remaining); - result = await r.read(readBuf); - } - if (result !== null) { - totalRead += result; - } - finished = totalRead === contentLength; - return result; - } - return { read }; -} - -export function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader { - // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 - const tp = new TextProtoReader(r); - let finished = false; - const chunks: Array<{ - offset: number; - data: Uint8Array; - }> = []; - async function read(buf: Uint8Array): Promise { - if (finished) return null; - const [chunk] = chunks; - if (chunk) { - const chunkRemaining = chunk.data.byteLength - chunk.offset; - const readLength = Math.min(chunkRemaining, buf.byteLength); - for (let i = 0; i < readLength; i++) { - buf[i] = chunk.data[chunk.offset + i]; - } - chunk.offset += readLength; - if (chunk.offset === chunk.data.byteLength) { - chunks.shift(); - // Consume \r\n; - if ((await tp.readLine()) === null) { - throw new Deno.errors.UnexpectedEof(); - } - } - return readLength; - } - const line = await tp.readLine(); - if (line === null) throw new Deno.errors.UnexpectedEof(); - // TODO: handle chunk extension - const [chunkSizeString] = line.split(";"); - const chunkSize = parseInt(chunkSizeString, 16); - if (Number.isNaN(chunkSize) || chunkSize < 0) { - throw new Error("Invalid chunk size"); - } - if (chunkSize > 0) { - if (chunkSize > buf.byteLength) { - let eof = await r.readFull(buf); - if (eof === null) { - throw new Deno.errors.UnexpectedEof(); - } - const restChunk = new Uint8Array(chunkSize - buf.byteLength); - eof = await r.readFull(restChunk); - if (eof === null) { - throw new Deno.errors.UnexpectedEof(); - } else { - chunks.push({ - offset: 0, - data: restChunk, - }); - } - return buf.byteLength; - } else { - const bufToFill = buf.subarray(0, chunkSize); - const eof = await r.readFull(bufToFill); - if (eof === null) { - throw new Deno.errors.UnexpectedEof(); - } - // Consume \r\n - if ((await tp.readLine()) === null) { - throw new Deno.errors.UnexpectedEof(); - } - return chunkSize; - } - } else { - assert(chunkSize === 0); - // Consume \r\n - if ((await r.readLine()) === null) { - throw new Deno.errors.UnexpectedEof(); - } - await readTrailers(h, r); - finished = true; - return null; - } - } - return { read }; -} - -const kProhibitedTrailerHeaders = [ - "transfer-encoding", - "content-length", - "trailer", -]; - -/** - * Read trailer headers from reader and append values to headers. - * "trailer" field will be deleted. - * */ -export async function readTrailers( - headers: Headers, - r: BufReader -): Promise { - const keys = parseTrailer(headers.get("trailer")); - if (!keys) return; - const tp = new TextProtoReader(r); - const result = await tp.readMIMEHeader(); - assert(result !== null, "trailer must be set"); - for (const [k, v] of result) { - if (!keys.has(k)) { - throw new Error("Undeclared trailer field"); - } - keys.delete(k); - headers.append(k, v); - } - assert(keys.size === 0, "Missing trailers"); - headers.delete("trailer"); -} - -function parseTrailer(field: string | null): Set | undefined { - if (field == null) { - return undefined; - } - const keys = field.split(",").map((v) => v.trim()); - if (keys.length === 0) { - throw new Error("Empty trailer"); - } - for (const invalid of kProhibitedTrailerHeaders) { - if (keys.includes(invalid)) { - throw new Error(`Prohibited field for trailer`); - } - } - return new Set(keys); -} - -export async function writeChunkedBody( - w: Deno.Writer, - r: Deno.Reader -): Promise { - const writer = BufWriter.create(w); - for await (const chunk of Deno.iter(r)) { - if (chunk.byteLength <= 0) continue; - const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); - const end = encoder.encode("\r\n"); - await writer.write(start); - await writer.write(chunk); - await writer.write(end); - } - - const endChunk = encoder.encode("0\r\n\r\n"); - await writer.write(endChunk); -} - -/** write trailer headers to writer. it mostly should be called after writeResponse */ -export async function writeTrailers( - w: Deno.Writer, - headers: Headers, - trailers: Headers -): Promise { - const trailer = headers.get("trailer"); - if (trailer === null) { - throw new Error('response headers must have "trailer" header field'); - } - const transferEncoding = headers.get("transfer-encoding"); - if (transferEncoding === null || !transferEncoding.match(/^chunked/)) { - throw new Error( - `trailer headers is only allowed for "transfer-encoding: chunked": got "${transferEncoding}"` - ); - } - const writer = BufWriter.create(w); - const trailerHeaderFields = trailer - .split(",") - .map((s) => s.trim().toLowerCase()); - for (const f of trailerHeaderFields) { - assert( - !kProhibitedTrailerHeaders.includes(f), - `"${f}" is prohibited for trailer header` - ); - } - for (const [key, value] of trailers) { - assert( - trailerHeaderFields.includes(key), - `Not trailer header field: ${key}` - ); - await writer.write(encoder.encode(`${key}: ${value}\r\n`)); - } - await writer.write(encoder.encode("\r\n")); - await writer.flush(); -} - -export async function writeResponse( - w: Deno.Writer, - r: Response -): Promise { - const protoMajor = 1; - const protoMinor = 1; - const statusCode = r.status || 200; - const statusText = STATUS_TEXT.get(statusCode); - const writer = BufWriter.create(w); - if (!statusText) { - throw new Deno.errors.InvalidData("Bad status code"); - } - if (!r.body) { - r.body = new Uint8Array(); - } - if (typeof r.body === "string") { - r.body = encoder.encode(r.body); - } - - let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`; - - const headers = r.headers ?? new Headers(); - - if (r.body && !headers.get("content-length")) { - if (r.body instanceof Uint8Array) { - out += `content-length: ${r.body.byteLength}\r\n`; - } else if (!headers.get("transfer-encoding")) { - out += "transfer-encoding: chunked\r\n"; - } - } - - for (const [key, value] of headers) { - out += `${key}: ${value}\r\n`; - } - - out += `\r\n`; - - const header = encoder.encode(out); - const n = await writer.write(header); - assert(n === header.byteLength); - - if (r.body instanceof Uint8Array) { - const n = await writer.write(r.body); - assert(n === r.body.byteLength); - } else if (headers.has("content-length")) { - const contentLength = headers.get("content-length"); - assert(contentLength != null); - const bodyLength = parseInt(contentLength); - const n = await Deno.copy(r.body, writer); - assert(n === bodyLength); - } else { - await writeChunkedBody(writer, r.body); - } - if (r.trailers) { - const t = await r.trailers(); - await writeTrailers(writer, headers, t); - } - await writer.flush(); -} - -/** - * ParseHTTPVersion parses a HTTP version string. - * "HTTP/1.0" returns (1, 0). - * Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request.go#L766-L792 - */ -export function parseHTTPVersion(vers: string): [number, number] { - switch (vers) { - case "HTTP/1.1": - return [1, 1]; - - case "HTTP/1.0": - return [1, 0]; - - default: { - const Big = 1000000; // arbitrary upper bound - - if (!vers.startsWith("HTTP/")) { - break; - } - - const dot = vers.indexOf("."); - if (dot < 0) { - break; - } - - const majorStr = vers.substring(vers.indexOf("/") + 1, dot); - const major = Number(majorStr); - if (!Number.isInteger(major) || major < 0 || major > Big) { - break; - } - - const minorStr = vers.substring(dot + 1); - const minor = Number(minorStr); - if (!Number.isInteger(minor) || minor < 0 || minor > Big) { - break; - } - - return [major, minor]; - } - } - - throw new Error(`malformed HTTP version ${vers}`); -} - -export async function readRequest( - conn: Deno.Conn, - bufr: BufReader -): Promise { - const tp = new TextProtoReader(bufr); - const firstLine = await tp.readLine(); // e.g. GET /index.html HTTP/1.0 - if (firstLine === null) return null; - const headers = await tp.readMIMEHeader(); - if (headers === null) throw new Deno.errors.UnexpectedEof(); - - const req = new ServerRequest(); - req.conn = conn; - req.r = bufr; - [req.method, req.url, req.proto] = firstLine.split(" ", 3); - [req.protoMinor, req.protoMajor] = parseHTTPVersion(req.proto); - req.headers = headers; - fixLength(req); - return req; -} - -function fixLength(req: ServerRequest): void { - const contentLength = req.headers.get("Content-Length"); - if (contentLength) { - const arrClen = contentLength.split(","); - if (arrClen.length > 1) { - const distinct = [...new Set(arrClen.map((e): string => e.trim()))]; - if (distinct.length > 1) { - throw Error("cannot contain multiple Content-Length headers"); - } else { - req.headers.set("Content-Length", distinct[0]); - } - } - const c = req.headers.get("Content-Length"); - if (req.method === "HEAD" && c && c !== "0") { - throw Error("http: method cannot contain a Content-Length"); - } - if (c && req.headers.has("transfer-encoding")) { - // A sender MUST NOT send a Content-Length header field in any message - // that contains a Transfer-Encoding header field. - // rfc: https://tools.ietf.org/html/rfc7230#section-3.3.2 - throw new Error( - "http: Transfer-Encoding and Content-Length cannot be send together" - ); - } - } -} diff --git a/std/http/io_test.ts b/std/http/io_test.ts deleted file mode 100644 index c0f57a1b7..000000000 --- a/std/http/io_test.ts +++ /dev/null @@ -1,476 +0,0 @@ -import { - AssertionError, - assertThrowsAsync, - assertEquals, - assert, - assertNotEquals, -} from "../testing/asserts.ts"; -import { - bodyReader, - writeTrailers, - readTrailers, - parseHTTPVersion, - readRequest, - writeResponse, -} from "./io.ts"; -import { encode, decode } from "../encoding/utf8.ts"; -import { BufReader, ReadLineResult } from "../io/bufio.ts"; -import { chunkedBodyReader } from "./io.ts"; -import { ServerRequest, Response } from "./server.ts"; -import { StringReader } from "../io/readers.ts"; -import { mockConn } from "./mock.ts"; -const { Buffer, test, readAll } = Deno; - -test("bodyReader", async () => { - const text = "Hello, Deno"; - const r = bodyReader(text.length, new BufReader(new Buffer(encode(text)))); - assertEquals(decode(await Deno.readAll(r)), text); -}); -function chunkify(n: number, char: string): string { - const v = Array.from({ length: n }) - .map(() => `${char}`) - .join(""); - return `${n.toString(16)}\r\n${v}\r\n`; -} -test("chunkedBodyReader", async () => { - const body = [ - chunkify(3, "a"), - chunkify(5, "b"), - chunkify(11, "c"), - chunkify(22, "d"), - chunkify(0, ""), - ].join(""); - const h = new Headers(); - const r = chunkedBodyReader(h, new BufReader(new Buffer(encode(body)))); - let result: number | null; - // Use small buffer as some chunks exceed buffer size - const buf = new Uint8Array(5); - const dest = new Buffer(); - while ((result = await r.read(buf)) !== null) { - const len = Math.min(buf.byteLength, result); - await dest.write(buf.subarray(0, len)); - } - const exp = "aaabbbbbcccccccccccdddddddddddddddddddddd"; - assertEquals(new TextDecoder().decode(dest.bytes()), exp); -}); - -test("chunkedBodyReader with trailers", async () => { - const body = [ - chunkify(3, "a"), - chunkify(5, "b"), - chunkify(11, "c"), - chunkify(22, "d"), - chunkify(0, ""), - "deno: land\r\n", - "node: js\r\n", - "\r\n", - ].join(""); - const h = new Headers({ - trailer: "deno,node", - }); - const r = chunkedBodyReader(h, new BufReader(new Buffer(encode(body)))); - assertEquals(h.has("trailer"), true); - assertEquals(h.has("deno"), false); - assertEquals(h.has("node"), false); - const act = decode(await Deno.readAll(r)); - const exp = "aaabbbbbcccccccccccdddddddddddddddddddddd"; - assertEquals(act, exp); - assertEquals(h.has("trailer"), false); - assertEquals(h.get("deno"), "land"); - assertEquals(h.get("node"), "js"); -}); - -test("readTrailers", async () => { - const h = new Headers({ - trailer: "deno,node", - }); - const trailer = ["deno: land", "node: js", "", ""].join("\r\n"); - await readTrailers(h, new BufReader(new Buffer(encode(trailer)))); - assertEquals(h.has("trailer"), false); - assertEquals(h.get("deno"), "land"); - assertEquals(h.get("node"), "js"); -}); - -test("readTrailer should throw if undeclared headers found in trailer", async () => { - const patterns = [ - ["deno,node", "deno: land\r\nnode: js\r\ngo: lang\r\n\r\n"], - ["deno", "node: js\r\n\r\n"], - ["deno", "node:js\r\ngo: lang\r\n\r\n"], - ]; - for (const [header, trailer] of patterns) { - const h = new Headers({ - trailer: header, - }); - await assertThrowsAsync( - async () => { - await readTrailers(h, new BufReader(new Buffer(encode(trailer)))); - }, - Error, - "Undeclared trailer field" - ); - } -}); - -test("readTrailer should throw if trailer contains prohibited fields", async () => { - for (const f of ["content-length", "trailer", "transfer-encoding"]) { - const h = new Headers({ - trailer: f, - }); - await assertThrowsAsync( - async () => { - await readTrailers(h, new BufReader(new Buffer())); - }, - Error, - "Prohibited field for trailer" - ); - } -}); - -test("writeTrailer", async () => { - const w = new Buffer(); - await writeTrailers( - w, - new Headers({ "transfer-encoding": "chunked", trailer: "deno,node" }), - new Headers({ deno: "land", node: "js" }) - ); - assertEquals( - new TextDecoder().decode(w.bytes()), - "deno: land\r\nnode: js\r\n\r\n" - ); -}); - -test("writeTrailer should throw", async () => { - const w = new Buffer(); - await assertThrowsAsync( - () => { - return writeTrailers(w, new Headers(), new Headers()); - }, - Error, - 'must have "trailer"' - ); - await assertThrowsAsync( - () => { - return writeTrailers(w, new Headers({ trailer: "deno" }), new Headers()); - }, - Error, - "only allowed" - ); - for (const f of ["content-length", "trailer", "transfer-encoding"]) { - await assertThrowsAsync( - () => { - return writeTrailers( - w, - new Headers({ "transfer-encoding": "chunked", trailer: f }), - new Headers({ [f]: "1" }) - ); - }, - AssertionError, - "prohibited" - ); - } - await assertThrowsAsync( - () => { - return writeTrailers( - w, - new Headers({ "transfer-encoding": "chunked", trailer: "deno" }), - new Headers({ node: "js" }) - ); - }, - AssertionError, - "Not trailer" - ); -}); - -// Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request_test.go#L535-L565 -test("parseHttpVersion", (): void => { - const testCases = [ - { in: "HTTP/0.9", want: [0, 9] }, - { in: "HTTP/1.0", want: [1, 0] }, - { in: "HTTP/1.1", want: [1, 1] }, - { in: "HTTP/3.14", want: [3, 14] }, - { in: "HTTP", err: true }, - { in: "HTTP/one.one", err: true }, - { in: "HTTP/1.1/", err: true }, - { in: "HTTP/-1.0", err: true }, - { in: "HTTP/0.-1", err: true }, - { in: "HTTP/", err: true }, - { in: "HTTP/1,0", err: true }, - { in: "HTTP/1.1000001", err: true }, - ]; - for (const t of testCases) { - let r, err; - try { - r = parseHTTPVersion(t.in); - } catch (e) { - err = e; - } - if (t.err) { - assert(err instanceof Error, t.in); - } else { - assertEquals(err, undefined); - assertEquals(r, t.want, t.in); - } - } -}); - -test("writeUint8ArrayResponse", async function (): Promise { - const shortText = "Hello"; - - const body = new TextEncoder().encode(shortText); - const res: Response = { body }; - - const buf = new Deno.Buffer(); - await writeResponse(buf, res); - - const decoder = new TextDecoder("utf-8"); - const reader = new BufReader(buf); - - let r: ReadLineResult | null = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), `content-length: ${shortText.length}`); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(r.line.byteLength, 0); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), shortText); - assertEquals(r.more, false); - - const eof = await reader.readLine(); - assertEquals(eof, null); -}); - -test("writeStringResponse", async function (): Promise { - const body = "Hello"; - - const res: Response = { body }; - - const buf = new Deno.Buffer(); - await writeResponse(buf, res); - - const decoder = new TextDecoder("utf-8"); - const reader = new BufReader(buf); - - let r: ReadLineResult | null = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), `content-length: ${body.length}`); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(r.line.byteLength, 0); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), body); - assertEquals(r.more, false); - - const eof = await reader.readLine(); - assertEquals(eof, null); -}); - -test("writeStringReaderResponse", async function (): Promise { - const shortText = "Hello"; - - const body = new StringReader(shortText); - const res: Response = { body }; - - const buf = new Deno.Buffer(); - await writeResponse(buf, res); - - const decoder = new TextDecoder("utf-8"); - const reader = new BufReader(buf); - - let r: ReadLineResult | null = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), "transfer-encoding: chunked"); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(r.line.byteLength, 0); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), shortText.length.toString()); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), shortText); - assertEquals(r.more, false); - - r = await reader.readLine(); - assert(r !== null); - assertEquals(decoder.decode(r.line), "0"); - assertEquals(r.more, false); -}); - -test("writeResponse with trailer", async () => { - const w = new Buffer(); - const body = new StringReader("Hello"); - await writeResponse(w, { - status: 200, - headers: new Headers({ - "transfer-encoding": "chunked", - trailer: "deno,node", - }), - body, - trailers: () => new Headers({ deno: "land", node: "js" }), - }); - const ret = new TextDecoder().decode(w.bytes()); - const exp = [ - "HTTP/1.1 200 OK", - "transfer-encoding: chunked", - "trailer: deno,node", - "", - "5", - "Hello", - "0", - "", - "deno: land", - "node: js", - "", - "", - ].join("\r\n"); - assertEquals(ret, exp); -}); - -test("writeResponseShouldNotModifyOriginHeaders", async () => { - const headers = new Headers(); - const buf = new Deno.Buffer(); - - await writeResponse(buf, { body: "foo", headers }); - assert(decode(await readAll(buf)).includes("content-length: 3")); - - await writeResponse(buf, { body: "hello", headers }); - assert(decode(await readAll(buf)).includes("content-length: 5")); -}); - -test("readRequestError", async function (): Promise { - const input = `GET / HTTP/1.1 -malformedHeader -`; - const reader = new BufReader(new StringReader(input)); - let err; - try { - await readRequest(mockConn(), reader); - } catch (e) { - err = e; - } - assert(err instanceof Error); - assertEquals(err.message, "malformed MIME header line: malformedHeader"); -}); - -// Ported from Go -// https://github.com/golang/go/blob/go1.12.5/src/net/http/request_test.go#L377-L443 -// TODO(zekth) fix tests -test("testReadRequestError", async function (): Promise { - const testCases = [ - { - in: "GET / HTTP/1.1\r\nheader: foo\r\n\r\n", - headers: [{ key: "header", value: "foo" }], - }, - { - in: "GET / HTTP/1.1\r\nheader:foo\r\n", - err: Deno.errors.UnexpectedEof, - }, - { in: "", eof: true }, - { - in: "HEAD / HTTP/1.1\r\nContent-Length:4\r\n\r\n", - err: "http: method cannot contain a Content-Length", - }, - { - in: "HEAD / HTTP/1.1\r\n\r\n", - headers: [], - }, - // Multiple Content-Length values should either be - // deduplicated if same or reject otherwise - // See Issue 16490. - { - in: - "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 0\r\n\r\n" + - "Gopher hey\r\n", - err: "cannot contain multiple Content-Length headers", - }, - { - in: - "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 6\r\n\r\n" + - "Gopher\r\n", - err: "cannot contain multiple Content-Length headers", - }, - { - in: - "PUT / HTTP/1.1\r\nContent-Length: 6 \r\nContent-Length: 6\r\n" + - "Content-Length:6\r\n\r\nGopher\r\n", - headers: [{ key: "Content-Length", value: "6" }], - }, - { - in: "PUT / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 6 \r\n\r\n", - err: "cannot contain multiple Content-Length headers", - }, - // Setting an empty header is swallowed by textproto - // see: readMIMEHeader() - // { - // in: "POST / HTTP/1.1\r\nContent-Length:\r\nContent-Length: 3\r\n\r\n", - // err: "cannot contain multiple Content-Length headers" - // }, - { - in: "HEAD / HTTP/1.1\r\nContent-Length:0\r\nContent-Length: 0\r\n\r\n", - headers: [{ key: "Content-Length", value: "0" }], - }, - { - in: - "POST / HTTP/1.1\r\nContent-Length:0\r\ntransfer-encoding: " + - "chunked\r\n\r\n", - headers: [], - err: "http: Transfer-Encoding and Content-Length cannot be send together", - }, - ]; - for (const test of testCases) { - const reader = new BufReader(new StringReader(test.in)); - let err; - let req: ServerRequest | null = null; - try { - req = await readRequest(mockConn(), reader); - } catch (e) { - err = e; - } - if (test.eof) { - assertEquals(req, null); - } else if (typeof test.err === "string") { - assertEquals(err.message, test.err); - } else if (test.err) { - assert(err instanceof (test.err as typeof Deno.errors.UnexpectedEof)); - } else { - assert(req instanceof ServerRequest); - assert(test.headers); - assertEquals(err, undefined); - assertNotEquals(req, null); - for (const h of test.headers) { - assertEquals(req.headers.get(h.key), h.value); - } - } - } -}); diff --git a/std/http/mock.ts b/std/http/mock.ts deleted file mode 100644 index be07ede24..000000000 --- a/std/http/mock.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** Create dummy Deno.Conn object with given base properties */ -export function mockConn(base: Partial = {}): Deno.Conn { - return { - localAddr: { - transport: "tcp", - hostname: "", - port: 0, - }, - remoteAddr: { - transport: "tcp", - hostname: "", - port: 0, - }, - rid: -1, - closeWrite: (): void => {}, - read: (): Promise => { - return Promise.resolve(0); - }, - write: (): Promise => { - return Promise.resolve(-1); - }, - close: (): void => {}, - ...base, - }; -} diff --git a/std/http/racing_server.ts b/std/http/racing_server.ts index 0b0e5a8a5..67db029e0 100644 --- a/std/http/racing_server.ts +++ b/std/http/racing_server.ts @@ -1,6 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { serve, ServerRequest } from "./server.ts"; -import { delay } from "../util/async.ts"; +import { delay } from "../async/delay.ts"; const addr = Deno.args[1] || "127.0.0.1:4501"; const server = serve(addr); diff --git a/std/http/server.ts b/std/http/server.ts index 9c678ad3d..a372b39a5 100644 --- a/std/http/server.ts +++ b/std/http/server.ts @@ -2,14 +2,14 @@ import { encode } from "../encoding/utf8.ts"; import { BufReader, BufWriter } from "../io/bufio.ts"; import { assert } from "../testing/asserts.ts"; -import { deferred, Deferred, MuxAsyncIterator } from "../util/async.ts"; +import { deferred, Deferred, MuxAsyncIterator } from "../async/mod.ts"; import { bodyReader, chunkedBodyReader, emptyReader, writeResponse, readRequest, -} from "./io.ts"; +} from "./_io.ts"; import Listener = Deno.Listener; import Conn = Deno.Conn; import Reader = Deno.Reader; diff --git a/std/http/server_test.ts b/std/http/server_test.ts index 03256dd25..807695c6b 100644 --- a/std/http/server_test.ts +++ b/std/http/server_test.ts @@ -15,9 +15,9 @@ import { } from "../testing/asserts.ts"; import { Response, ServerRequest, Server, serve } from "./server.ts"; import { BufReader, BufWriter } from "../io/bufio.ts"; -import { delay } from "../util/async.ts"; +import { delay } from "../async/delay.ts"; import { encode, decode } from "../encoding/utf8.ts"; -import { mockConn } from "./mock.ts"; +import { mockConn } from "./_mock_conn.ts"; const { Buffer, test } = Deno; -- cgit v1.2.3