diff options
Diffstat (limited to 'std/http')
-rw-r--r-- | std/http/README.md | 71 | ||||
-rw-r--r-- | std/http/_io.ts | 380 | ||||
-rw-r--r-- | std/http/_io_test.ts | 494 | ||||
-rw-r--r-- | std/http/_mock_conn.ts | 29 | ||||
-rw-r--r-- | std/http/bench.ts | 17 | ||||
-rw-r--r-- | std/http/cookie.ts | 204 | ||||
-rw-r--r-- | std/http/cookie_test.ts | 316 | ||||
-rw-r--r-- | std/http/file_server.ts | 454 | ||||
-rw-r--r-- | std/http/file_server_test.ts | 465 | ||||
-rw-r--r-- | std/http/http_status.ts | 197 | ||||
-rw-r--r-- | std/http/mod.ts | 4 | ||||
-rw-r--r-- | std/http/racing_server.ts | 68 | ||||
-rw-r--r-- | std/http/racing_server_test.ts | 81 | ||||
-rw-r--r-- | std/http/server.ts | 399 | ||||
-rw-r--r-- | std/http/server_test.ts | 784 | ||||
-rw-r--r-- | std/http/test.ts | 2 | ||||
-rw-r--r-- | std/http/testdata/% | 0 | ||||
-rw-r--r-- | std/http/testdata/file_server_as_library.ts | 12 | ||||
-rw-r--r-- | std/http/testdata/hello.html | 0 | ||||
-rw-r--r-- | std/http/testdata/simple_https_server.ts | 18 | ||||
-rw-r--r-- | std/http/testdata/simple_server.ts | 9 | ||||
-rw-r--r-- | std/http/testdata/test file.txt | 0 | ||||
l--------- | std/http/testdata/tls | 1 |
23 files changed, 0 insertions, 4005 deletions
diff --git a/std/http/README.md b/std/http/README.md deleted file mode 100644 index c2c4c8ce6..000000000 --- a/std/http/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# http - -```typescript -import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts"; -const server = serve({ port: 8000 }); -console.log("http://localhost:8000/"); -for await (const req of server) { - req.respond({ body: "Hello World\n" }); -} -``` - -### File Server - -A small program for serving local files over HTTP. - -```sh -deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts -> HTTP server listening on http://0.0.0.0:4507/ -``` - -## Cookie - -Helper to manipulate `Cookie` through `ServerRequest` and `Response`. - -```ts -import { ServerRequest } from "https://deno.land/std@$STD_VERSION/http/server.ts"; -import { getCookies } from "https://deno.land/std@$STD_VERSION/http/cookie.ts"; - -let request = new ServerRequest(); -request.headers = new Headers(); -request.headers.set("Cookie", "full=of; tasty=chocolate"); - -const cookies = getCookies(request); -console.log("cookies:", cookies); -// cookies: { full: "of", tasty: "chocolate" } -``` - -To set a `Cookie` you can add `CookieOptions` to properly set your `Cookie`: - -```ts -import { Response } from "https://deno.land/std@$STD_VERSION/http/server.ts"; -import { - Cookie, - setCookie, -} from "https://deno.land/std@$STD_VERSION/http/cookie.ts"; - -let response: Response = {}; -const cookie: Cookie = { name: "Space", value: "Cat" }; -setCookie(response, cookie); - -const cookieHeader = response.headers.get("set-cookie"); -console.log("Set-Cookie:", cookieHeader); -// Set-Cookie: Space=Cat -``` - -Deleting a `Cookie` will set its expiration date before now. Forcing the browser -to delete it. - -```ts -import { Response } from "https://deno.land/std@$STD_VERSION/http/server.ts"; -import { deleteCookie } from "https://deno.land/std@$STD_VERSION/http/cookie.ts"; - -let response: Response = {}; -deleteCookie(response, "deno"); - -const cookieHeader = response.headers.get("set-cookie"); -console.log("Set-Cookie:", cookieHeader); -// Set-Cookie: deno=; Expires=Thus, 01 Jan 1970 00:00:00 GMT -``` - -**Note**: At the moment multiple `Set-Cookie` in a `Response` is not handled. diff --git a/std/http/_io.ts b/std/http/_io.ts deleted file mode 100644 index 529f59cb5..000000000 --- a/std/http/_io.ts +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { BufReader, BufWriter } from "../io/bufio.ts"; -import { TextProtoReader } from "../textproto/mod.ts"; -import { assert } from "../_util/assert.ts"; -import { encoder } from "../encoding/utf8.ts"; -import { Response, ServerRequest } from "./server.ts"; -import { STATUS_TEXT } from "./http_status.ts"; - -export function emptyReader(): Deno.Reader { - return { - read(_: Uint8Array): Promise<number | null> { - 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<number | null> { - 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<number | null> { - 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(bartlomieju): handle chunk extension - const [chunkSizeString] = line.split(";"); - const chunkSize = parseInt(chunkSizeString, 16); - if (Number.isNaN(chunkSize) || chunkSize < 0) { - throw new Deno.errors.InvalidData("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 }; -} - -function isProhibidedForTrailer(key: string): boolean { - const s = new Set(["transfer-encoding", "content-length", "trailer"]); - return s.has(key.toLowerCase()); -} - -/** Read trailer headers from reader and append values to headers. "trailer" - * field will be deleted. */ -export async function readTrailers( - headers: Headers, - r: BufReader, -): Promise<void> { - const trailers = parseTrailer(headers.get("trailer")); - if (trailers == null) return; - const trailerNames = [...trailers.keys()]; - const tp = new TextProtoReader(r); - const result = await tp.readMIMEHeader(); - if (result == null) { - throw new Deno.errors.InvalidData("Missing trailer header."); - } - const undeclared = [...result.keys()].filter( - (k) => !trailerNames.includes(k), - ); - if (undeclared.length > 0) { - throw new Deno.errors.InvalidData( - `Undeclared trailers: ${Deno.inspect(undeclared)}.`, - ); - } - for (const [k, v] of result) { - headers.append(k, v); - } - const missingTrailers = trailerNames.filter((k) => !result.has(k)); - if (missingTrailers.length > 0) { - throw new Deno.errors.InvalidData( - `Missing trailers: ${Deno.inspect(missingTrailers)}.`, - ); - } - headers.delete("trailer"); -} - -function parseTrailer(field: string | null): Headers | undefined { - if (field == null) { - return undefined; - } - const trailerNames = field.split(",").map((v) => v.trim().toLowerCase()); - if (trailerNames.length === 0) { - throw new Deno.errors.InvalidData("Empty trailer header."); - } - const prohibited = trailerNames.filter((k) => isProhibidedForTrailer(k)); - if (prohibited.length > 0) { - throw new Deno.errors.InvalidData( - `Prohibited trailer names: ${Deno.inspect(prohibited)}.`, - ); - } - return new Headers(trailerNames.map((key) => [key, ""])); -} - -export async function writeChunkedBody( - w: BufWriter, - r: Deno.Reader, -): Promise<void> { - 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 w.write(start); - await w.write(chunk); - await w.write(end); - await w.flush(); - } - - const endChunk = encoder.encode("0\r\n\r\n"); - await w.write(endChunk); -} - -/** Write trailer headers to writer. It should mostly should be called after - * `writeResponse()`. */ -export async function writeTrailers( - w: Deno.Writer, - headers: Headers, - trailers: Headers, -): Promise<void> { - const trailer = headers.get("trailer"); - if (trailer === null) { - throw new TypeError("Missing trailer header."); - } - const transferEncoding = headers.get("transfer-encoding"); - if (transferEncoding === null || !transferEncoding.match(/^chunked/)) { - throw new TypeError( - `Trailers are only allowed for "transfer-encoding: chunked", got "transfer-encoding: ${transferEncoding}".`, - ); - } - const writer = BufWriter.create(w); - const trailerNames = trailer.split(",").map((s) => s.trim().toLowerCase()); - const prohibitedTrailers = trailerNames.filter((k) => - isProhibidedForTrailer(k) - ); - if (prohibitedTrailers.length > 0) { - throw new TypeError( - `Prohibited trailer names: ${Deno.inspect(prohibitedTrailers)}.`, - ); - } - const undeclared = [...trailers.keys()].filter( - (k) => !trailerNames.includes(k), - ); - if (undeclared.length > 0) { - throw new TypeError(`Undeclared trailers: ${Deno.inspect(undeclared)}.`); - } - for (const [key, value] of trailers) { - 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<void> { - 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<ServerRequest | null> { - 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.protoMajor, req.protoMinor] = 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 ea3d282b8..000000000 --- a/std/http/_io_test.ts +++ /dev/null @@ -1,494 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { - assert, - assertEquals, - assertNotEquals, - assertThrowsAsync, -} from "../testing/asserts.ts"; -import { - bodyReader, - chunkedBodyReader, - parseHTTPVersion, - readRequest, - readTrailers, - writeResponse, - writeTrailers, -} from "./_io.ts"; -import { decode, encode } from "../encoding/utf8.ts"; -import { BufReader, ReadLineResult } from "../io/bufio.ts"; -import { Response, ServerRequest } from "./server.ts"; -import { StringReader } from "../io/readers.ts"; -import { mockConn } from "./_mock_conn.ts"; - -Deno.test("bodyReader", async () => { - const text = "Hello, Deno"; - const r = bodyReader( - text.length, - new BufReader(new Deno.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`; -} -Deno.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 Deno.Buffer(encode(body)))); - let result: number | null; - // Use small buffer as some chunks exceed buffer size - const buf = new Uint8Array(5); - const dest = new Deno.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); -}); - -Deno.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 Deno.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"); -}); - -Deno.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 Deno.Buffer(encode(trailer)))); - assertEquals(h.has("trailer"), false); - assertEquals(h.get("deno"), "land"); - assertEquals(h.get("node"), "js"); -}); - -Deno.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 Deno.Buffer(encode(trailer))), - ); - }, - Deno.errors.InvalidData, - `Undeclared trailers: [ "`, - ); - } - }, -); - -Deno.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 Deno.Buffer())); - }, - Deno.errors.InvalidData, - `Prohibited trailer names: [ "`, - ); - } - }, -); - -Deno.test("writeTrailer", async () => { - const w = new Deno.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", - ); -}); - -Deno.test("writeTrailer should throw", async () => { - const w = new Deno.Buffer(); - await assertThrowsAsync( - () => { - return writeTrailers(w, new Headers(), new Headers()); - }, - TypeError, - "Missing trailer header.", - ); - await assertThrowsAsync( - () => { - return writeTrailers(w, new Headers({ trailer: "deno" }), new Headers()); - }, - TypeError, - `Trailers are only allowed for "transfer-encoding: chunked", got "transfer-encoding: null".`, - ); - 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" }), - ); - }, - TypeError, - `Prohibited trailer names: [ "`, - ); - } - await assertThrowsAsync( - () => { - return writeTrailers( - w, - new Headers({ "transfer-encoding": "chunked", trailer: "deno" }), - new Headers({ node: "js" }), - ); - }, - TypeError, - `Undeclared trailers: [ "node" ].`, - ); -}); - -// Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request_test.go#L535-L565 -Deno.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); - } - } -}); - -Deno.test("writeUint8ArrayResponse", async function (): Promise<void> { - 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); -}); - -Deno.test("writeStringResponse", async function (): Promise<void> { - 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); -}); - -Deno.test("writeStringReaderResponse", async function (): Promise<void> { - 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); -}); - -Deno.test("writeResponse with trailer", async () => { - const w = new Deno.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); -}); - -Deno.test("writeResponseShouldNotModifyOriginHeaders", async () => { - const headers = new Headers(); - const buf = new Deno.Buffer(); - - await writeResponse(buf, { body: "foo", headers }); - assert(decode(await Deno.readAll(buf)).includes("content-length: 3")); - - await writeResponse(buf, { body: "hello", headers }); - assert(decode(await Deno.readAll(buf)).includes("content-length: 5")); -}); - -Deno.test("readRequestError", async function (): Promise<void> { - 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 -Deno.test("testReadRequestError", async function (): Promise<void> { - 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: "POST / HTTP/1.0\r\n\r\n", - headers: [], - version: true, - }, - { 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); - if (test.version) { - // return value order of parseHTTPVersion() function have to match with [req.protoMajor, req.protoMinor]; - const version = parseHTTPVersion(test.in.split(" ", 3)[2]); - assertEquals(req.protoMajor, version[0]); - assertEquals(req.protoMinor, version[1]); - } - 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 deleted file mode 100644 index 8f3396dbd..000000000 --- a/std/http/_mock_conn.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. - -/** Create dummy Deno.Conn object with given base properties */ -export function mockConn(base: Partial<Deno.Conn> = {}): Deno.Conn { - return { - localAddr: { - transport: "tcp", - hostname: "", - port: 0, - }, - remoteAddr: { - transport: "tcp", - hostname: "", - port: 0, - }, - rid: -1, - closeWrite: (): Promise<void> => { - return Promise.resolve(); - }, - read: (): Promise<number | null> => { - return Promise.resolve(0); - }, - write: (): Promise<number> => { - return Promise.resolve(-1); - }, - close: (): void => {}, - ...base, - }; -} diff --git a/std/http/bench.ts b/std/http/bench.ts deleted file mode 100644 index 5ba95ef0a..000000000 --- a/std/http/bench.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { serve } from "./server.ts"; - -const addr = Deno.args[0] || "127.0.0.1:4500"; -const server = serve(addr); -const body = new TextEncoder().encode("Hello World"); - -console.log(`http://${addr}/`); -for await (const req of server) { - const res = { - body, - headers: new Headers(), - }; - res.headers.set("Date", new Date().toUTCString()); - res.headers.set("Connection", "keep-alive"); - req.respond(res).catch(() => {}); -} diff --git a/std/http/cookie.ts b/std/http/cookie.ts deleted file mode 100644 index 486afd6c6..000000000 --- a/std/http/cookie.ts +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -// Structured similarly to Go's cookie.go -// https://github.com/golang/go/blob/master/src/net/http/cookie.go -import { assert } from "../_util/assert.ts"; -import { toIMF } from "../datetime/mod.ts"; - -export type Cookies = Record<string, string>; - -export interface Cookie { - /** Name of the cookie. */ - name: string; - /** Value of the cookie. */ - value: string; - /** Expiration date of the cookie. */ - expires?: Date; - /** Max-Age of the Cookie. Must be integer superior to 0. */ - maxAge?: number; - /** Specifies those hosts to which the cookie will be sent. */ - domain?: string; - /** Indicates a URL path that must exist in the request. */ - path?: string; - /** Indicates if the cookie is made using SSL & HTTPS. */ - secure?: boolean; - /** Indicates that cookie is not accessible via JavaScript. **/ - httpOnly?: boolean; - /** Allows servers to assert that a cookie ought not to - * be sent along with cross-site requests. */ - sameSite?: SameSite; - /** Additional key value pairs with the form "key=value" */ - unparsed?: string[]; -} - -export type SameSite = "Strict" | "Lax" | "None"; - -const FIELD_CONTENT_REGEXP = /^(?=[\x20-\x7E]*$)[^()@<>,;:\\"\[\]?={}\s]+$/; - -function toString(cookie: Cookie): string { - if (!cookie.name) { - return ""; - } - const out: string[] = []; - validateCookieName(cookie.name); - validateCookieValue(cookie.name, cookie.value); - out.push(`${cookie.name}=${cookie.value}`); - - // Fallback for invalid Set-Cookie - // ref: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1 - if (cookie.name.startsWith("__Secure")) { - cookie.secure = true; - } - if (cookie.name.startsWith("__Host")) { - cookie.path = "/"; - cookie.secure = true; - delete cookie.domain; - } - - if (cookie.secure) { - out.push("Secure"); - } - if (cookie.httpOnly) { - out.push("HttpOnly"); - } - if (typeof cookie.maxAge === "number" && Number.isInteger(cookie.maxAge)) { - assert(cookie.maxAge > 0, "Max-Age must be an integer superior to 0"); - out.push(`Max-Age=${cookie.maxAge}`); - } - if (cookie.domain) { - out.push(`Domain=${cookie.domain}`); - } - if (cookie.sameSite) { - out.push(`SameSite=${cookie.sameSite}`); - } - if (cookie.path) { - validatePath(cookie.path); - out.push(`Path=${cookie.path}`); - } - if (cookie.expires) { - const dateString = toIMF(cookie.expires); - out.push(`Expires=${dateString}`); - } - if (cookie.unparsed) { - out.push(cookie.unparsed.join("; ")); - } - return out.join("; "); -} - -/** - * Validate Cookie Name. - * @param name Cookie name. - */ -function validateCookieName(name: string | undefined | null): void { - if (name && !FIELD_CONTENT_REGEXP.test(name)) { - throw new TypeError(`Invalid cookie name: "${name}".`); - } -} - -/** - * Validate Path Value. - * @see https://tools.ietf.org/html/rfc6265#section-4.1.2.4 - * @param path Path value. - */ -function validatePath(path: string | null): void { - if (path == null) { - return; - } - for (let i = 0; i < path.length; i++) { - const c = path.charAt(i); - if ( - c < String.fromCharCode(0x20) || c > String.fromCharCode(0x7E) || c == ";" - ) { - throw new Error( - path + ": Invalid cookie path char '" + c + "'", - ); - } - } -} - -/** - *Validate Cookie Value. - * @see https://tools.ietf.org/html/rfc6265#section-4.1 - * @param value Cookie value. - */ -function validateCookieValue(name: string, value: string | null): void { - if (value == null || name == null) return; - for (let i = 0; i < value.length; i++) { - const c = value.charAt(i); - if ( - c < String.fromCharCode(0x21) || c == String.fromCharCode(0x22) || - c == String.fromCharCode(0x2c) || c == String.fromCharCode(0x3b) || - c == String.fromCharCode(0x5c) || c == String.fromCharCode(0x7f) - ) { - throw new Error( - "RFC2616 cookie '" + name + "' cannot have '" + c + "' as value", - ); - } - if (c > String.fromCharCode(0x80)) { - throw new Error( - "RFC2616 cookie '" + name + "' can only have US-ASCII chars as value" + - c.charCodeAt(0).toString(16), - ); - } - } -} - -/** - * Parse the cookies of the Server Request - * @param req An object which has a `headers` property - */ -export function getCookies(req: { headers: Headers }): Cookies { - const cookie = req.headers.get("Cookie"); - if (cookie != null) { - const out: Cookies = {}; - const c = cookie.split(";"); - for (const kv of c) { - const [cookieKey, ...cookieVal] = kv.split("="); - assert(cookieKey != null); - const key = cookieKey.trim(); - out[key] = cookieVal.join("="); - } - return out; - } - return {}; -} - -/** - * Set the cookie header properly in the Response - * @param res An object which has a headers property - * @param cookie Cookie to set - * - * Example: - * - * ```ts - * setCookie(response, { name: 'deno', value: 'runtime', - * httpOnly: true, secure: true, maxAge: 2, domain: "deno.land" }); - * ``` - */ -export function setCookie(res: { headers?: Headers }, cookie: Cookie): void { - if (!res.headers) { - res.headers = new Headers(); - } - // TODO(zekth) : Add proper parsing of Set-Cookie headers - // Parsing cookie headers to make consistent set-cookie header - // ref: https://tools.ietf.org/html/rfc6265#section-4.1.1 - const v = toString(cookie); - if (v) { - res.headers.append("Set-Cookie", v); - } -} - -/** - * Set the cookie header properly in the Response to delete it - * @param res Server Response - * @param name Name of the cookie to Delete - * Example: - * - * deleteCookie(res,'foo'); - */ -export function deleteCookie(res: { headers?: Headers }, name: string): void { - setCookie(res, { - name: name, - value: "", - expires: new Date(0), - }); -} diff --git a/std/http/cookie_test.ts b/std/http/cookie_test.ts deleted file mode 100644 index 1973eed01..000000000 --- a/std/http/cookie_test.ts +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { Response, ServerRequest } from "./server.ts"; -import { deleteCookie, getCookies, setCookie } from "./cookie.ts"; -import { assert, assertEquals, assertThrows } from "../testing/asserts.ts"; - -Deno.test({ - name: "Cookie parser", - fn(): void { - const req = new ServerRequest(); - req.headers = new Headers(); - assertEquals(getCookies(req), {}); - req.headers = new Headers(); - req.headers.set("Cookie", "foo=bar"); - assertEquals(getCookies(req), { foo: "bar" }); - - req.headers = new Headers(); - req.headers.set("Cookie", "full=of ; tasty=chocolate"); - assertEquals(getCookies(req), { full: "of ", tasty: "chocolate" }); - - req.headers = new Headers(); - req.headers.set("Cookie", "igot=99; problems=but..."); - assertEquals(getCookies(req), { igot: "99", problems: "but..." }); - - req.headers = new Headers(); - req.headers.set("Cookie", "PREF=al=en-GB&f1=123; wide=1; SID=123"); - assertEquals(getCookies(req), { - PREF: "al=en-GB&f1=123", - wide: "1", - SID: "123", - }); - }, -}); - -Deno.test({ - name: "Cookie Name Validation", - fn(): void { - const res: Response = {}; - const tokens = [ - '"id"', - "id\t", - "i\td", - "i d", - "i;d", - "{id}", - "[id]", - '"', - "id\u0091", - ]; - res.headers = new Headers(); - tokens.forEach((name) => { - assertThrows( - (): void => { - setCookie(res, { - name, - value: "Cat", - httpOnly: true, - secure: true, - maxAge: 3, - }); - }, - Error, - 'Invalid cookie name: "' + name + '".', - ); - }); - }, -}); - -Deno.test({ - name: "Cookie Value Validation", - fn(): void { - const res: Response = {}; - const tokens = [ - "1f\tWa", - "\t", - "1f Wa", - "1f;Wa", - '"1fWa', - "1f\\Wa", - '1f"Wa', - '"', - "1fWa\u0005", - "1f\u0091Wa", - ]; - res.headers = new Headers(); - tokens.forEach((value) => { - assertThrows( - (): void => { - setCookie( - res, - { - name: "Space", - value, - httpOnly: true, - secure: true, - maxAge: 3, - }, - ); - }, - Error, - "RFC2616 cookie 'Space'", - ); - }); - }, -}); - -Deno.test({ - name: "Cookie Path Validation", - fn(): void { - const res: Response = {}; - const path = "/;domain=sub.domain.com"; - res.headers = new Headers(); - assertThrows( - (): void => { - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - path, - maxAge: 3, - }); - }, - Error, - path + ": Invalid cookie path char ';'", - ); - }, -}); - -Deno.test({ - name: "Cookie Delete", - fn(): void { - const res: Response = {}; - deleteCookie(res, "deno"); - assertEquals( - res.headers?.get("Set-Cookie"), - "deno=; Expires=Thu, 01 Jan 1970 00:00:00 GMT", - ); - }, -}); - -Deno.test({ - name: "Cookie Set", - fn(): void { - const res: Response = {}; - - res.headers = new Headers(); - setCookie(res, { name: "Space", value: "Cat" }); - assertEquals(res.headers.get("Set-Cookie"), "Space=Cat"); - - res.headers = new Headers(); - setCookie(res, { name: "Space", value: "Cat", secure: true }); - assertEquals(res.headers.get("Set-Cookie"), "Space=Cat; Secure"); - - res.headers = new Headers(); - setCookie(res, { name: "Space", value: "Cat", httpOnly: true }); - assertEquals(res.headers.get("Set-Cookie"), "Space=Cat; HttpOnly"); - - res.headers = new Headers(); - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - }); - assertEquals(res.headers.get("Set-Cookie"), "Space=Cat; Secure; HttpOnly"); - - res.headers = new Headers(); - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - maxAge: 2, - }); - assertEquals( - res.headers.get("Set-Cookie"), - "Space=Cat; Secure; HttpOnly; Max-Age=2", - ); - - let error = false; - res.headers = new Headers(); - try { - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - maxAge: 0, - }); - } catch (e) { - error = true; - } - assert(error); - - res.headers = new Headers(); - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - maxAge: 2, - domain: "deno.land", - }); - assertEquals( - res.headers.get("Set-Cookie"), - "Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land", - ); - - res.headers = new Headers(); - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - maxAge: 2, - domain: "deno.land", - sameSite: "Strict", - }); - assertEquals( - res.headers.get("Set-Cookie"), - "Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; " + - "SameSite=Strict", - ); - - res.headers = new Headers(); - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - maxAge: 2, - domain: "deno.land", - sameSite: "Lax", - }); - assertEquals( - res.headers.get("Set-Cookie"), - "Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; SameSite=Lax", - ); - - res.headers = new Headers(); - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - maxAge: 2, - domain: "deno.land", - path: "/", - }); - assertEquals( - res.headers.get("Set-Cookie"), - "Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/", - ); - - res.headers = new Headers(); - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - maxAge: 2, - domain: "deno.land", - path: "/", - unparsed: ["unparsed=keyvalue", "batman=Bruce"], - }); - assertEquals( - res.headers.get("Set-Cookie"), - "Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; " + - "unparsed=keyvalue; batman=Bruce", - ); - - res.headers = new Headers(); - setCookie(res, { - name: "Space", - value: "Cat", - httpOnly: true, - secure: true, - maxAge: 2, - domain: "deno.land", - path: "/", - expires: new Date(Date.UTC(1983, 0, 7, 15, 32)), - }); - assertEquals( - res.headers.get("Set-Cookie"), - "Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; " + - "Expires=Fri, 07 Jan 1983 15:32:00 GMT", - ); - - res.headers = new Headers(); - setCookie(res, { name: "__Secure-Kitty", value: "Meow" }); - assertEquals(res.headers.get("Set-Cookie"), "__Secure-Kitty=Meow; Secure"); - - res.headers = new Headers(); - setCookie(res, { - name: "__Host-Kitty", - value: "Meow", - domain: "deno.land", - }); - assertEquals( - res.headers.get("Set-Cookie"), - "__Host-Kitty=Meow; Secure; Path=/", - ); - - res.headers = new Headers(); - setCookie(res, { name: "cookie-1", value: "value-1", secure: true }); - setCookie(res, { name: "cookie-2", value: "value-2", maxAge: 3600 }); - assertEquals( - res.headers.get("Set-Cookie"), - "cookie-1=value-1; Secure, cookie-2=value-2; Max-Age=3600", - ); - - res.headers = new Headers(); - setCookie(res, { name: "", value: "" }); - assertEquals(res.headers.get("Set-Cookie"), null); - }, -}); diff --git a/std/http/file_server.ts b/std/http/file_server.ts deleted file mode 100644 index 8fd2e7484..000000000 --- a/std/http/file_server.ts +++ /dev/null @@ -1,454 +0,0 @@ -#!/usr/bin/env -S deno run --allow-net --allow-read -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. - -// This program serves files in the current directory over HTTP. -// TODO(bartlomieju): Stream responses instead of reading them into memory. -// TODO(bartlomieju): Add tests like these: -// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js - -import { extname, posix } from "../path/mod.ts"; -import { - HTTPSOptions, - listenAndServe, - listenAndServeTLS, - Response, - ServerRequest, -} from "./server.ts"; -import { parse } from "../flags/mod.ts"; -import { assert } from "../_util/assert.ts"; - -interface EntryInfo { - mode: string; - size: string; - url: string; - name: string; -} - -export interface FileServerArgs { - _: string[]; - // -p --port - p?: number; - port?: number; - // --cors - cors?: boolean; - // --no-dir-listing - "dir-listing"?: boolean; - // --host - host?: string; - // -c --cert - c?: string; - cert?: string; - // -k --key - k?: string; - key?: string; - // -h --help - h?: boolean; - help?: boolean; -} - -const encoder = new TextEncoder(); - -const serverArgs = parse(Deno.args) as FileServerArgs; -const target = posix.resolve(serverArgs._[0] ?? ""); - -const MEDIA_TYPES: Record<string, string> = { - ".md": "text/markdown", - ".html": "text/html", - ".htm": "text/html", - ".json": "application/json", - ".map": "application/json", - ".txt": "text/plain", - ".ts": "text/typescript", - ".tsx": "text/tsx", - ".js": "application/javascript", - ".jsx": "text/jsx", - ".gz": "application/gzip", - ".css": "text/css", - ".wasm": "application/wasm", - ".mjs": "application/javascript", -}; - -/** Returns the content-type based on the extension of a path. */ -function contentType(path: string): string | undefined { - return MEDIA_TYPES[extname(path)]; -} - -function modeToString(isDir: boolean, maybeMode: number | null): string { - const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; - - if (maybeMode === null) { - return "(unknown mode)"; - } - const mode = maybeMode.toString(8); - if (mode.length < 3) { - return "(unknown mode)"; - } - let output = ""; - mode - .split("") - .reverse() - .slice(0, 3) - .forEach((v): void => { - output = modeMap[+v] + output; - }); - output = `(${isDir ? "d" : "-"}${output})`; - return output; -} - -function fileLenToString(len: number): string { - const multiplier = 1024; - let base = 1; - const suffix = ["B", "K", "M", "G", "T"]; - let suffixIndex = 0; - - while (base * multiplier < len) { - if (suffixIndex >= suffix.length - 1) { - break; - } - base *= multiplier; - suffixIndex++; - } - - return `${(len / base).toFixed(2)}${suffix[suffixIndex]}`; -} - -/** - * Returns an HTTP Response with the requested file as the body - * @param req The server request context used to cleanup the file handle - * @param filePath Path of the file to serve - */ -export async function serveFile( - req: ServerRequest, - filePath: string, -): Promise<Response> { - const [file, fileInfo] = await Promise.all([ - Deno.open(filePath), - Deno.stat(filePath), - ]); - const headers = new Headers(); - headers.set("content-length", fileInfo.size.toString()); - const contentTypeValue = contentType(filePath); - if (contentTypeValue) { - headers.set("content-type", contentTypeValue); - } - req.done.then(() => { - file.close(); - }); - return { - status: 200, - body: file, - headers, - }; -} - -// TODO(bartlomieju): simplify this after deno.stat and deno.readDir are fixed -async function serveDir( - req: ServerRequest, - dirPath: string, -): Promise<Response> { - const dirUrl = `/${posix.relative(target, dirPath)}`; - const listEntry: EntryInfo[] = []; - for await (const entry of Deno.readDir(dirPath)) { - const filePath = posix.join(dirPath, entry.name); - const fileUrl = posix.join(dirUrl, entry.name); - if (entry.name === "index.html" && entry.isFile) { - // in case index.html as dir... - return serveFile(req, filePath); - } - // Yuck! - let fileInfo = null; - try { - fileInfo = await Deno.stat(filePath); - } catch (e) { - // Pass - } - listEntry.push({ - mode: modeToString(entry.isDirectory, fileInfo?.mode ?? null), - size: entry.isFile ? fileLenToString(fileInfo?.size ?? 0) : "", - name: entry.name, - url: fileUrl, - }); - } - listEntry.sort((a, b) => - a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1 - ); - const formattedDirUrl = `${dirUrl.replace(/\/$/, "")}/`; - const page = encoder.encode(dirViewerTemplate(formattedDirUrl, listEntry)); - - const headers = new Headers(); - headers.set("content-type", "text/html"); - - const res = { - status: 200, - body: page, - headers, - }; - return res; -} - -function serveFallback(req: ServerRequest, e: Error): Promise<Response> { - if (e instanceof URIError) { - return Promise.resolve({ - status: 400, - body: encoder.encode("Bad Request"), - }); - } else if (e instanceof Deno.errors.NotFound) { - return Promise.resolve({ - status: 404, - body: encoder.encode("Not Found"), - }); - } else { - return Promise.resolve({ - status: 500, - body: encoder.encode("Internal server error"), - }); - } -} - -function serverLog(req: ServerRequest, res: Response): void { - const d = new Date().toISOString(); - const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`; - const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`; - console.log(s); -} - -function setCORS(res: Response): void { - if (!res.headers) { - res.headers = new Headers(); - } - res.headers.append("access-control-allow-origin", "*"); - res.headers.append( - "access-control-allow-headers", - "Origin, X-Requested-With, Content-Type, Accept, Range", - ); -} - -function dirViewerTemplate(dirname: string, entries: EntryInfo[]): string { - return html` - <!DOCTYPE html> - <html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta http-equiv="X-UA-Compatible" content="ie=edge" /> - <title>Deno File Server</title> - <style> - :root { - --background-color: #fafafa; - --color: rgba(0, 0, 0, 0.87); - } - @media (prefers-color-scheme: dark) { - :root { - --background-color: #303030; - --color: #fff; - } - } - @media (min-width: 960px) { - main { - max-width: 960px; - } - body { - padding-left: 32px; - padding-right: 32px; - } - } - @media (min-width: 600px) { - main { - padding-left: 24px; - padding-right: 24px; - } - } - body { - background: var(--background-color); - color: var(--color); - font-family: "Roboto", "Helvetica", "Arial", sans-serif; - font-weight: 400; - line-height: 1.43; - font-size: 0.875rem; - } - a { - color: #2196f3; - text-decoration: none; - } - a:hover { - text-decoration: underline; - } - table th { - text-align: left; - } - table td { - padding: 12px 24px 0 0; - } - </style> - </head> - <body> - <main> - <h1>Index of ${dirname}</h1> - <table> - <tr> - <th>Mode</th> - <th>Size</th> - <th>Name</th> - </tr> - ${ - entries.map( - (entry) => - html` - <tr> - <td class="mode"> - ${entry.mode} - </td> - <td> - ${entry.size} - </td> - <td> - <a href="${entry.url}">${entry.name}</a> - </td> - </tr> - `, - ) - } - </table> - </main> - </body> - </html> - `; -} - -function html(strings: TemplateStringsArray, ...values: unknown[]): string { - const l = strings.length - 1; - let html = ""; - - for (let i = 0; i < l; i++) { - let v = values[i]; - if (v instanceof Array) { - v = v.join(""); - } - const s = strings[i] + v; - html += s; - } - html += strings[l]; - return html; -} - -function normalizeURL(url: string): string { - let normalizedUrl = url; - try { - normalizedUrl = decodeURI(normalizedUrl); - } catch (e) { - if (!(e instanceof URIError)) { - throw e; - } - } - - try { - //allowed per https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html - const absoluteURI = new URL(normalizedUrl); - normalizedUrl = absoluteURI.pathname; - } catch (e) { //wasn't an absoluteURI - if (!(e instanceof TypeError)) { - throw e; - } - } - - if (normalizedUrl[0] !== "/") { - throw new URIError("The request URI is malformed."); - } - - normalizedUrl = posix.normalize(normalizedUrl); - const startOfParams = normalizedUrl.indexOf("?"); - return startOfParams > -1 - ? normalizedUrl.slice(0, startOfParams) - : normalizedUrl; -} - -function main(): void { - const CORSEnabled = serverArgs.cors ? true : false; - const port = serverArgs.port ?? serverArgs.p ?? 4507; - const host = serverArgs.host ?? "0.0.0.0"; - const addr = `${host}:${port}`; - const tlsOpts = {} as HTTPSOptions; - tlsOpts.certFile = serverArgs.cert ?? serverArgs.c ?? ""; - tlsOpts.keyFile = serverArgs.key ?? serverArgs.k ?? ""; - const dirListingEnabled = serverArgs["dir-listing"] ?? true; - - if (tlsOpts.keyFile || tlsOpts.certFile) { - if (tlsOpts.keyFile === "" || tlsOpts.certFile === "") { - console.log("--key and --cert are required for TLS"); - serverArgs.h = true; - } - } - - if (serverArgs.h ?? serverArgs.help) { - console.log(`Deno File Server - Serves a local directory in HTTP. - - INSTALL: - deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts - - USAGE: - file_server [path] [options] - - OPTIONS: - -h, --help Prints help information - -p, --port <PORT> Set port - --cors Enable CORS via the "Access-Control-Allow-Origin" header - --host <HOST> Hostname (default is 0.0.0.0) - -c, --cert <FILE> TLS certificate file (enables TLS) - -k, --key <FILE> TLS key file (enables TLS) - --no-dir-listing Disable directory listing - - All TLS options are required when one is provided.`); - Deno.exit(); - } - - const handler = async (req: ServerRequest): Promise<void> => { - let response: Response | undefined; - try { - const normalizedUrl = normalizeURL(req.url); - let fsPath = posix.join(target, normalizedUrl); - if (fsPath.indexOf(target) !== 0) { - fsPath = target; - } - const fileInfo = await Deno.stat(fsPath); - if (fileInfo.isDirectory) { - if (dirListingEnabled) { - response = await serveDir(req, fsPath); - } else { - throw new Deno.errors.NotFound(); - } - } else { - response = await serveFile(req, fsPath); - } - } catch (e) { - console.error(e.message); - response = await serveFallback(req, e); - } finally { - if (CORSEnabled) { - assert(response); - setCORS(response); - } - serverLog(req, response!); - try { - await req.respond(response!); - } catch (e) { - console.error(e.message); - } - } - }; - - let proto = "http"; - if (tlsOpts.keyFile || tlsOpts.certFile) { - proto += "s"; - tlsOpts.hostname = host; - tlsOpts.port = port; - listenAndServeTLS(tlsOpts, handler); - } else { - listenAndServe(addr, handler); - } - console.log(`${proto.toUpperCase()} server listening on ${proto}://${addr}/`); -} - -if (import.meta.main) { - main(); -} diff --git a/std/http/file_server_test.ts b/std/http/file_server_test.ts deleted file mode 100644 index 638121b45..000000000 --- a/std/http/file_server_test.ts +++ /dev/null @@ -1,465 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { - assert, - assertEquals, - assertNotEquals, - assertStringIncludes, -} from "../testing/asserts.ts"; -import { BufReader } from "../io/bufio.ts"; -import { TextProtoReader } from "../textproto/mod.ts"; -import { Response, ServerRequest } from "./server.ts"; -import { FileServerArgs, serveFile } from "./file_server.ts"; -import { dirname, fromFileUrl, join, resolve } from "../path/mod.ts"; -let fileServer: Deno.Process<Deno.RunOptions & { stdout: "piped" }>; - -type FileServerCfg = Omit<FileServerArgs, "_"> & { target?: string }; - -const moduleDir = dirname(fromFileUrl(import.meta.url)); -const testdataDir = resolve(moduleDir, "testdata"); - -async function startFileServer({ - target = ".", - port = 4507, - "dir-listing": dirListing = true, -}: FileServerCfg = {}): Promise<void> { - fileServer = Deno.run({ - cmd: [ - Deno.execPath(), - "run", - "--quiet", - "--allow-read", - "--allow-net", - "file_server.ts", - target, - "--cors", - "-p", - `${port}`, - `${dirListing ? "" : "--no-dir-listing"}`, - ], - cwd: moduleDir, - stdout: "piped", - stderr: "null", - }); - // Once fileServer is ready it will write to its stdout. - assert(fileServer.stdout != null); - const r = new TextProtoReader(new BufReader(fileServer.stdout)); - const s = await r.readLine(); - assert(s !== null && s.includes("server listening")); -} - -async function startFileServerAsLibrary({}: FileServerCfg = {}): Promise<void> { - fileServer = await Deno.run({ - cmd: [ - Deno.execPath(), - "run", - "--quiet", - "--allow-read", - "--allow-net", - "testdata/file_server_as_library.ts", - ], - cwd: moduleDir, - stdout: "piped", - stderr: "null", - }); - assert(fileServer.stdout != null); - const r = new TextProtoReader(new BufReader(fileServer.stdout)); - const s = await r.readLine(); - assert(s !== null && s.includes("Server running...")); -} - -async function killFileServer(): Promise<void> { - fileServer.close(); - // Process.close() kills the file server process. However this termination - // happens asynchronously, and since we've just closed the process resource, - // we can't use `await fileServer.status()` to wait for the process to have - // exited. As a workaround, wait for its stdout to close instead. - // TODO(piscisaureus): when `Process.kill()` is stable and works on Windows, - // switch to calling `kill()` followed by `await fileServer.status()`. - await Deno.readAll(fileServer.stdout!); - fileServer.stdout!.close(); -} - -interface StringResponse extends Response { - body: string; -} - -/* HTTP GET request allowing arbitrary paths */ -async function fetchExactPath( - hostname: string, - port: number, - path: string, -): Promise<StringResponse> { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - const request = encoder.encode("GET " + path + " HTTP/1.1\r\n\r\n"); - let conn: void | Deno.Conn; - try { - conn = await Deno.connect( - { hostname: hostname, port: port, transport: "tcp" }, - ); - await Deno.writeAll(conn, request); - let currentResult = ""; - let contentLength = -1; - let startOfBody = -1; - for await (const chunk of Deno.iter(conn)) { - currentResult += decoder.decode(chunk); - if (contentLength === -1) { - const match = /^content-length: (.*)$/m.exec(currentResult); - if (match && match[1]) { - contentLength = Number(match[1]); - } - } - if (startOfBody === -1) { - const ind = currentResult.indexOf("\r\n\r\n"); - if (ind !== -1) { - startOfBody = ind + 4; - } - } - if (startOfBody !== -1 && contentLength !== -1) { - const byteLen = encoder.encode(currentResult).length; - if (byteLen >= contentLength + startOfBody) { - break; - } - } - } - const status = /^HTTP\/1.1 (...)/.exec(currentResult); - let statusCode = 0; - if (status && status[1]) { - statusCode = Number(status[1]); - } - - const body = currentResult.slice(startOfBody); - const headersStr = currentResult.slice(0, startOfBody); - const headersReg = /^(.*): (.*)$/mg; - const headersObj: { [i: string]: string } = {}; - let match = headersReg.exec(headersStr); - while (match !== null) { - if (match[1] && match[2]) { - headersObj[match[1]] = match[2]; - } - match = headersReg.exec(headersStr); - } - return { - status: statusCode, - headers: new Headers(headersObj), - body: body, - }; - } finally { - if (conn) { - Deno.close(conn.rid); - } - } -} - -Deno.test( - "file_server serveFile", - async (): Promise<void> => { - await startFileServer(); - try { - const res = await fetch("http://localhost:4507/README.md"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEquals(res.headers.get("content-type"), "text/markdown"); - const downloadedFile = await res.text(); - const localFile = new TextDecoder().decode( - await Deno.readFile(join(moduleDir, "README.md")), - ); - assertEquals(downloadedFile, localFile); - } finally { - await killFileServer(); - } - }, -); - -Deno.test( - "file_server serveFile in testdata", - async (): Promise<void> => { - await startFileServer({ target: "./testdata" }); - try { - const res = await fetch("http://localhost:4507/hello.html"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEquals(res.headers.get("content-type"), "text/html"); - const downloadedFile = await res.text(); - const localFile = new TextDecoder().decode( - await Deno.readFile(join(testdataDir, "hello.html")), - ); - assertEquals(downloadedFile, localFile); - } finally { - await killFileServer(); - } - }, -); - -Deno.test("serveDirectory", async function (): Promise<void> { - await startFileServer(); - try { - const res = await fetch("http://localhost:4507/"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - const page = await res.text(); - assert(page.includes("README.md")); - - // `Deno.FileInfo` is not completely compatible with Windows yet - // TODO(bartlomieju): `mode` should work correctly in the future. - // Correct this test case accordingly. - Deno.build.os !== "windows" && - assert(/<td class="mode">(\s)*\([a-zA-Z-]{10}\)(\s)*<\/td>/.test(page)); - Deno.build.os === "windows" && - assert(/<td class="mode">(\s)*\(unknown mode\)(\s)*<\/td>/.test(page)); - assert(page.includes(`<a href="/README.md">README.md</a>`)); - } finally { - await killFileServer(); - } -}); - -Deno.test("serveFallback", async function (): Promise<void> { - await startFileServer(); - try { - const res = await fetch("http://localhost:4507/badfile.txt"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEquals(res.status, 404); - const _ = await res.text(); - } finally { - await killFileServer(); - } -}); - -Deno.test("checkPathTraversal", async function (): Promise<void> { - await startFileServer(); - try { - const res = await fetch( - "http://localhost:4507/../../../../../../../..", - ); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEquals(res.status, 200); - const listing = await res.text(); - assertStringIncludes(listing, "README.md"); - } finally { - await killFileServer(); - } -}); - -Deno.test("checkPathTraversalNoLeadingSlash", async function (): Promise<void> { - await startFileServer(); - try { - const res = await fetchExactPath("127.0.0.1", 4507, "../../../.."); - assertEquals(res.status, 400); - } finally { - await killFileServer(); - } -}); - -Deno.test("checkPathTraversalAbsoluteURI", async function (): Promise<void> { - await startFileServer(); - try { - //allowed per https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html - const res = await fetchExactPath( - "127.0.0.1", - 4507, - "http://localhost/../../../..", - ); - assertEquals(res.status, 200); - assertStringIncludes(res.body, "README.md"); - } finally { - await killFileServer(); - } -}); - -Deno.test("checkURIEncodedPathTraversal", async function (): Promise<void> { - await startFileServer(); - try { - const res = await fetch( - "http://localhost:4507/%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..", - ); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEquals(res.status, 404); - const _ = await res.text(); - } finally { - await killFileServer(); - } -}); - -Deno.test("serveWithUnorthodoxFilename", async function (): Promise<void> { - await startFileServer(); - try { - let res = await fetch("http://localhost:4507/testdata/%"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEquals(res.status, 200); - let _ = await res.text(); - res = await fetch("http://localhost:4507/testdata/test%20file.txt"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEquals(res.status, 200); - _ = await res.text(); - } finally { - await killFileServer(); - } -}); - -Deno.test("printHelp", async function (): Promise<void> { - const helpProcess = Deno.run({ - cmd: [ - Deno.execPath(), - "run", - "--quiet", - // TODO(ry) It ought to be possible to get the help output without - // --allow-read. - "--allow-read", - "file_server.ts", - "--help", - ], - cwd: moduleDir, - stdout: "piped", - }); - assert(helpProcess.stdout != null); - const r = new TextProtoReader(new BufReader(helpProcess.stdout)); - const s = await r.readLine(); - assert(s !== null && s.includes("Deno File Server")); - helpProcess.close(); - helpProcess.stdout.close(); -}); - -Deno.test("contentType", async () => { - const request = new ServerRequest(); - const response = await serveFile(request, join(testdataDir, "hello.html")); - const contentType = response.headers!.get("content-type"); - assertEquals(contentType, "text/html"); - (response.body as Deno.File).close(); -}); - -Deno.test("file_server running as library", async function (): Promise<void> { - await startFileServerAsLibrary(); - try { - const res = await fetch("http://localhost:8000"); - assertEquals(res.status, 200); - const _ = await res.text(); - } finally { - await killFileServer(); - } -}); - -Deno.test("file_server should ignore query params", async () => { - await startFileServer(); - try { - const res = await fetch("http://localhost:4507/README.md?key=value"); - assertEquals(res.status, 200); - const downloadedFile = await res.text(); - const localFile = new TextDecoder().decode( - await Deno.readFile(join(moduleDir, "README.md")), - ); - assertEquals(downloadedFile, localFile); - } finally { - await killFileServer(); - } -}); - -async function startTlsFileServer({ - target = ".", - port = 4577, -}: FileServerCfg = {}): Promise<void> { - fileServer = Deno.run({ - cmd: [ - Deno.execPath(), - "run", - "--quiet", - "--allow-read", - "--allow-net", - "file_server.ts", - target, - "--host", - "localhost", - "--cert", - "./testdata/tls/localhost.crt", - "--key", - "./testdata/tls/localhost.key", - "--cors", - "-p", - `${port}`, - ], - cwd: moduleDir, - stdout: "piped", - stderr: "null", - }); - // Once fileServer is ready it will write to its stdout. - assert(fileServer.stdout != null); - const r = new TextProtoReader(new BufReader(fileServer.stdout)); - const s = await r.readLine(); - assert(s !== null && s.includes("server listening")); -} - -Deno.test("serveDirectory TLS", async function (): Promise<void> { - await startTlsFileServer(); - try { - // Valid request after invalid - const conn = await Deno.connectTls({ - hostname: "localhost", - port: 4577, - certFile: join(testdataDir, "tls/RootCA.pem"), - }); - - await Deno.writeAll( - conn, - new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"), - ); - const res = new Uint8Array(128 * 1024); - const nread = await conn.read(res); - assert(nread !== null); - conn.close(); - const page = new TextDecoder().decode(res.subarray(0, nread)); - assert(page.includes("<title>Deno File Server</title>")); - } finally { - await killFileServer(); - } -}); - -Deno.test("partial TLS arguments fail", async function (): Promise<void> { - fileServer = Deno.run({ - cmd: [ - Deno.execPath(), - "run", - "--quiet", - "--allow-read", - "--allow-net", - "file_server.ts", - ".", - "--host", - "localhost", - "--cert", - "./testdata/tls/localhost.crt", - "-p", - `4578`, - ], - cwd: moduleDir, - stdout: "piped", - stderr: "null", - }); - try { - // Once fileServer is ready it will write to its stdout. - assert(fileServer.stdout != null); - const r = new TextProtoReader(new BufReader(fileServer.stdout)); - const s = await r.readLine(); - assert( - s !== null && s.includes("--key and --cert are required for TLS"), - ); - } finally { - await killFileServer(); - } -}); - -Deno.test("file_server disable dir listings", async function (): Promise<void> { - await startFileServer({ "dir-listing": false }); - try { - const res = await fetch("http://localhost:4507/"); - assert(res.headers.has("access-control-allow-origin")); - assert(res.headers.has("access-control-allow-headers")); - assertEquals(res.status, 404); - const _ = await res.text(); - } finally { - await killFileServer(); - } -}); diff --git a/std/http/http_status.ts b/std/http/http_status.ts deleted file mode 100644 index a6148b2f7..000000000 --- a/std/http/http_status.ts +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. - -/** HTTP status codes */ -export enum Status { - /** RFC 7231, 6.2.1 */ - Continue = 100, - /** RFC 7231, 6.2.2 */ - SwitchingProtocols = 101, - /** RFC 2518, 10.1 */ - Processing = 102, - /** RFC 8297 **/ - EarlyHints = 103, - /** RFC 7231, 6.3.1 */ - OK = 200, - /** RFC 7231, 6.3.2 */ - Created = 201, - /** RFC 7231, 6.3.3 */ - Accepted = 202, - /** RFC 7231, 6.3.4 */ - NonAuthoritativeInfo = 203, - /** RFC 7231, 6.3.5 */ - NoContent = 204, - /** RFC 7231, 6.3.6 */ - ResetContent = 205, - /** RFC 7233, 4.1 */ - PartialContent = 206, - /** RFC 4918, 11.1 */ - MultiStatus = 207, - /** RFC 5842, 7.1 */ - AlreadyReported = 208, - /** RFC 3229, 10.4.1 */ - IMUsed = 226, - - /** RFC 7231, 6.4.1 */ - MultipleChoices = 300, - /** RFC 7231, 6.4.2 */ - MovedPermanently = 301, - /** RFC 7231, 6.4.3 */ - Found = 302, - /** RFC 7231, 6.4.4 */ - SeeOther = 303, - /** RFC 7232, 4.1 */ - NotModified = 304, - /** RFC 7231, 6.4.5 */ - UseProxy = 305, - /** RFC 7231, 6.4.7 */ - TemporaryRedirect = 307, - /** RFC 7538, 3 */ - PermanentRedirect = 308, - - /** RFC 7231, 6.5.1 */ - BadRequest = 400, - /** RFC 7235, 3.1 */ - Unauthorized = 401, - /** RFC 7231, 6.5.2 */ - PaymentRequired = 402, - /** RFC 7231, 6.5.3 */ - Forbidden = 403, - /** RFC 7231, 6.5.4 */ - NotFound = 404, - /** RFC 7231, 6.5.5 */ - MethodNotAllowed = 405, - /** RFC 7231, 6.5.6 */ - NotAcceptable = 406, - /** RFC 7235, 3.2 */ - ProxyAuthRequired = 407, - /** RFC 7231, 6.5.7 */ - RequestTimeout = 408, - /** RFC 7231, 6.5.8 */ - Conflict = 409, - /** RFC 7231, 6.5.9 */ - Gone = 410, - /** RFC 7231, 6.5.10 */ - LengthRequired = 411, - /** RFC 7232, 4.2 */ - PreconditionFailed = 412, - /** RFC 7231, 6.5.11 */ - RequestEntityTooLarge = 413, - /** RFC 7231, 6.5.12 */ - RequestURITooLong = 414, - /** RFC 7231, 6.5.13 */ - UnsupportedMediaType = 415, - /** RFC 7233, 4.4 */ - RequestedRangeNotSatisfiable = 416, - /** RFC 7231, 6.5.14 */ - ExpectationFailed = 417, - /** RFC 7168, 2.3.3 */ - Teapot = 418, - /** RFC 7540, 9.1.2 */ - MisdirectedRequest = 421, - /** RFC 4918, 11.2 */ - UnprocessableEntity = 422, - /** RFC 4918, 11.3 */ - Locked = 423, - /** RFC 4918, 11.4 */ - FailedDependency = 424, - /** RFC 8470, 5.2 */ - TooEarly = 425, - /** RFC 7231, 6.5.15 */ - UpgradeRequired = 426, - /** RFC 6585, 3 */ - PreconditionRequired = 428, - /** RFC 6585, 4 */ - TooManyRequests = 429, - /** RFC 6585, 5 */ - RequestHeaderFieldsTooLarge = 431, - /** RFC 7725, 3 */ - UnavailableForLegalReasons = 451, - - /** RFC 7231, 6.6.1 */ - InternalServerError = 500, - /** RFC 7231, 6.6.2 */ - NotImplemented = 501, - /** RFC 7231, 6.6.3 */ - BadGateway = 502, - /** RFC 7231, 6.6.4 */ - ServiceUnavailable = 503, - /** RFC 7231, 6.6.5 */ - GatewayTimeout = 504, - /** RFC 7231, 6.6.6 */ - HTTPVersionNotSupported = 505, - /** RFC 2295, 8.1 */ - VariantAlsoNegotiates = 506, - /** RFC 4918, 11.5 */ - InsufficientStorage = 507, - /** RFC 5842, 7.2 */ - LoopDetected = 508, - /** RFC 2774, 7 */ - NotExtended = 510, - /** RFC 6585, 6 */ - NetworkAuthenticationRequired = 511, -} - -export const STATUS_TEXT = new Map<Status, string>([ - [Status.Continue, "Continue"], - [Status.SwitchingProtocols, "Switching Protocols"], - [Status.Processing, "Processing"], - [Status.EarlyHints, "Early Hints"], - [Status.OK, "OK"], - [Status.Created, "Created"], - [Status.Accepted, "Accepted"], - [Status.NonAuthoritativeInfo, "Non-Authoritative Information"], - [Status.NoContent, "No Content"], - [Status.ResetContent, "Reset Content"], - [Status.PartialContent, "Partial Content"], - [Status.MultiStatus, "Multi-Status"], - [Status.AlreadyReported, "Already Reported"], - [Status.IMUsed, "IM Used"], - [Status.MultipleChoices, "Multiple Choices"], - [Status.MovedPermanently, "Moved Permanently"], - [Status.Found, "Found"], - [Status.SeeOther, "See Other"], - [Status.NotModified, "Not Modified"], - [Status.UseProxy, "Use Proxy"], - [Status.TemporaryRedirect, "Temporary Redirect"], - [Status.PermanentRedirect, "Permanent Redirect"], - [Status.BadRequest, "Bad Request"], - [Status.Unauthorized, "Unauthorized"], - [Status.PaymentRequired, "Payment Required"], - [Status.Forbidden, "Forbidden"], - [Status.NotFound, "Not Found"], - [Status.MethodNotAllowed, "Method Not Allowed"], - [Status.NotAcceptable, "Not Acceptable"], - [Status.ProxyAuthRequired, "Proxy Authentication Required"], - [Status.RequestTimeout, "Request Timeout"], - [Status.Conflict, "Conflict"], - [Status.Gone, "Gone"], - [Status.LengthRequired, "Length Required"], - [Status.PreconditionFailed, "Precondition Failed"], - [Status.RequestEntityTooLarge, "Request Entity Too Large"], - [Status.RequestURITooLong, "Request URI Too Long"], - [Status.UnsupportedMediaType, "Unsupported Media Type"], - [Status.RequestedRangeNotSatisfiable, "Requested Range Not Satisfiable"], - [Status.ExpectationFailed, "Expectation Failed"], - [Status.Teapot, "I'm a teapot"], - [Status.MisdirectedRequest, "Misdirected Request"], - [Status.UnprocessableEntity, "Unprocessable Entity"], - [Status.Locked, "Locked"], - [Status.FailedDependency, "Failed Dependency"], - [Status.TooEarly, "Too Early"], - [Status.UpgradeRequired, "Upgrade Required"], - [Status.PreconditionRequired, "Precondition Required"], - [Status.TooManyRequests, "Too Many Requests"], - [Status.RequestHeaderFieldsTooLarge, "Request Header Fields Too Large"], - [Status.UnavailableForLegalReasons, "Unavailable For Legal Reasons"], - [Status.InternalServerError, "Internal Server Error"], - [Status.NotImplemented, "Not Implemented"], - [Status.BadGateway, "Bad Gateway"], - [Status.ServiceUnavailable, "Service Unavailable"], - [Status.GatewayTimeout, "Gateway Timeout"], - [Status.HTTPVersionNotSupported, "HTTP Version Not Supported"], - [Status.VariantAlsoNegotiates, "Variant Also Negotiates"], - [Status.InsufficientStorage, "Insufficient Storage"], - [Status.LoopDetected, "Loop Detected"], - [Status.NotExtended, "Not Extended"], - [Status.NetworkAuthenticationRequired, "Network Authentication Required"], -]); diff --git a/std/http/mod.ts b/std/http/mod.ts deleted file mode 100644 index 827eaebf8..000000000 --- a/std/http/mod.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -export * from "./cookie.ts"; -export * from "./http_status.ts"; -export * from "./server.ts"; diff --git a/std/http/racing_server.ts b/std/http/racing_server.ts deleted file mode 100644 index b5cf69298..000000000 --- a/std/http/racing_server.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { serve, ServerRequest } from "./server.ts"; -import { delay } from "../async/delay.ts"; - -const addr = Deno.args[1] || "127.0.0.1:4501"; -const server = serve(addr); - -function body(i: number): string { - return `Step${i}\n`; -} -async function delayedRespond( - request: ServerRequest, - step: number, -): Promise<void> { - await delay(3000); - await request.respond({ status: 200, body: body(step) }); -} - -async function largeRespond(request: ServerRequest, c: string): Promise<void> { - const b = new Uint8Array(1024 * 1024); - b.fill(c.charCodeAt(0)); - await request.respond({ status: 200, body: b }); -} - -async function ignoreToConsume( - request: ServerRequest, - step: number, -): Promise<void> { - await request.respond({ status: 200, body: body(step) }); -} - -console.log("Racing server listening...\n"); - -let step = 1; -for await (const request of server) { - switch (step) { - case 1: - // Try to wait long enough. - // For pipelining, this should cause all the following response - // to block. - delayedRespond(request, step); - break; - case 2: - // HUGE body. - largeRespond(request, "a"); - break; - case 3: - // HUGE body. - largeRespond(request, "b"); - break; - case 4: - // Ignore to consume body (content-length) - ignoreToConsume(request, step); - break; - case 5: - // Ignore to consume body (chunked) - ignoreToConsume(request, step); - break; - case 6: - // Ignore to consume body (chunked + trailers) - ignoreToConsume(request, step); - break; - default: - request.respond({ status: 200, body: body(step) }); - break; - } - step++; -} diff --git a/std/http/racing_server_test.ts b/std/http/racing_server_test.ts deleted file mode 100644 index 8018a4312..000000000 --- a/std/http/racing_server_test.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { assert, assertEquals } from "../testing/asserts.ts"; -import { BufReader, BufWriter } from "../io/bufio.ts"; -import { TextProtoReader } from "../textproto/mod.ts"; -import { dirname, fromFileUrl } from "../path/mod.ts"; - -const moduleDir = dirname(fromFileUrl(import.meta.url)); - -let server: Deno.Process<Deno.RunOptions & { stdout: "piped" }>; -async function startServer(): Promise<void> { - server = Deno.run({ - cmd: [Deno.execPath(), "run", "--quiet", "-A", "racing_server.ts"], - cwd: moduleDir, - stdout: "piped", - }); - // Once racing server is ready it will write to its stdout. - assert(server.stdout != null); - const r = new TextProtoReader(new BufReader(server.stdout)); - const s = await r.readLine(); - assert(s !== null && s.includes("Racing server listening...")); -} -function killServer(): void { - server.close(); - server.stdout.close(); -} - -const input = [ - "GET / HTTP/1.1\r\n\r\n", - "GET / HTTP/1.1\r\n\r\n", - "GET / HTTP/1.1\r\n\r\n", - "POST / HTTP/1.1\r\ncontent-length: 4\r\n\r\ndeno", - "POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n4\r\ndeno\r\n0\r\n\r\n", - "POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\ntrailer: deno\r\n\r\n4\r\ndeno\r\n0\r\n\r\ndeno: land\r\n\r\n", - "GET / HTTP/1.1\r\n\r\n", -].join(""); -const HUGE_BODY_SIZE = 1024 * 1024; -const output = `HTTP/1.1 200 OK -content-length: 6 - -Step1 -HTTP/1.1 200 OK -content-length: ${HUGE_BODY_SIZE} - -${"a".repeat(HUGE_BODY_SIZE)}HTTP/1.1 200 OK -content-length: ${HUGE_BODY_SIZE} - -${"b".repeat(HUGE_BODY_SIZE)}HTTP/1.1 200 OK -content-length: 6 - -Step4 -HTTP/1.1 200 OK -content-length: 6 - -Step5 -HTTP/1.1 200 OK -content-length: 6 - -Step6 -HTTP/1.1 200 OK -content-length: 6 - -Step7 -`; - -Deno.test("serverPipelineRace", async function (): Promise<void> { - await startServer(); - - const conn = await Deno.connect({ port: 4501 }); - const r = new TextProtoReader(new BufReader(conn)); - const w = new BufWriter(conn); - await w.write(new TextEncoder().encode(input)); - await w.flush(); - const outLines = output.split("\n"); - // length - 1 to disregard last empty line - for (let i = 0; i < outLines.length - 1; i++) { - const s = await r.readLine(); - assertEquals(s, outLines[i]); - } - killServer(); - conn.close(); -}); diff --git a/std/http/server.ts b/std/http/server.ts deleted file mode 100644 index f17c759c4..000000000 --- a/std/http/server.ts +++ /dev/null @@ -1,399 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { encode } from "../encoding/utf8.ts"; -import { BufReader, BufWriter } from "../io/bufio.ts"; -import { assert } from "../_util/assert.ts"; -import { Deferred, deferred, MuxAsyncIterator } from "../async/mod.ts"; -import { - bodyReader, - chunkedBodyReader, - emptyReader, - readRequest, - writeResponse, -} from "./_io.ts"; - -export class ServerRequest { - url!: string; - method!: string; - proto!: string; - protoMinor!: number; - protoMajor!: number; - headers!: Headers; - conn!: Deno.Conn; - r!: BufReader; - w!: BufWriter; - - #done: Deferred<Error | undefined> = deferred(); - #contentLength?: number | null = undefined; - #body?: Deno.Reader = undefined; - #finalized = false; - - get done(): Promise<Error | undefined> { - return this.#done.then((e) => e); - } - - /** - * Value of Content-Length header. - * If null, then content length is invalid or not given (e.g. chunked encoding). - */ - get contentLength(): number | null { - // undefined means not cached. - // null means invalid or not provided. - if (this.#contentLength === undefined) { - const cl = this.headers.get("content-length"); - if (cl) { - this.#contentLength = parseInt(cl); - // Convert NaN to null (as NaN harder to test) - if (Number.isNaN(this.#contentLength)) { - this.#contentLength = null; - } - } else { - this.#contentLength = null; - } - } - return this.#contentLength; - } - - /** - * Body of the request. The easiest way to consume the body is: - * - * const buf: Uint8Array = await Deno.readAll(req.body); - */ - get body(): Deno.Reader { - if (!this.#body) { - if (this.contentLength != null) { - this.#body = bodyReader(this.contentLength, this.r); - } else { - const transferEncoding = this.headers.get("transfer-encoding"); - if (transferEncoding != null) { - const parts = transferEncoding - .split(",") - .map((e): string => e.trim().toLowerCase()); - assert( - parts.includes("chunked"), - 'transfer-encoding must include "chunked" if content-length is not set', - ); - this.#body = chunkedBodyReader(this.headers, this.r); - } else { - // Neither content-length nor transfer-encoding: chunked - this.#body = emptyReader(); - } - } - } - return this.#body; - } - - async respond(r: Response): Promise<void> { - let err: Error | undefined; - try { - // Write our response! - await writeResponse(this.w, r); - } catch (e) { - try { - // Eagerly close on error. - this.conn.close(); - } catch { - // Pass - } - err = e; - } - // Signal that this request has been processed and the next pipelined - // request on the same connection can be accepted. - this.#done.resolve(err); - if (err) { - // Error during responding, rethrow. - throw err; - } - } - - async finalize(): Promise<void> { - if (this.#finalized) return; - // Consume unread body - const body = this.body; - const buf = new Uint8Array(1024); - while ((await body.read(buf)) !== null) { - // Pass - } - this.#finalized = true; - } -} - -export class Server implements AsyncIterable<ServerRequest> { - #closing = false; - #connections: Deno.Conn[] = []; - - constructor(public listener: Deno.Listener) {} - - close(): void { - this.#closing = true; - this.listener.close(); - for (const conn of this.#connections) { - try { - conn.close(); - } catch (e) { - // Connection might have been already closed - if (!(e instanceof Deno.errors.BadResource)) { - throw e; - } - } - } - } - - // Yields all HTTP requests on a single TCP connection. - private async *iterateHttpRequests( - conn: Deno.Conn, - ): AsyncIterableIterator<ServerRequest> { - const reader = new BufReader(conn); - const writer = new BufWriter(conn); - - while (!this.#closing) { - let request: ServerRequest | null; - try { - request = await readRequest(conn, reader); - } catch (error) { - if ( - error instanceof Deno.errors.InvalidData || - error instanceof Deno.errors.UnexpectedEof - ) { - // An error was thrown while parsing request headers. - // Try to send the "400 Bad Request" before closing the connection. - try { - await writeResponse(writer, { - status: 400, - body: encode(`${error.message}\r\n\r\n`), - }); - } catch (error) { - // The connection is broken. - } - } - break; - } - if (request === null) { - break; - } - - request.w = writer; - yield request; - - // Wait for the request to be processed before we accept a new request on - // this connection. - const responseError = await request.done; - if (responseError) { - // Something bad happened during response. - // (likely other side closed during pipelined req) - // req.done implies this connection already closed, so we can just return. - this.untrackConnection(request.conn); - return; - } - - try { - // Consume unread body and trailers if receiver didn't consume those data - await request.finalize(); - } catch (error) { - // Invalid data was received or the connection was closed. - break; - } - } - - this.untrackConnection(conn); - try { - conn.close(); - } catch (e) { - // might have been already closed - } - } - - private trackConnection(conn: Deno.Conn): void { - this.#connections.push(conn); - } - - private untrackConnection(conn: Deno.Conn): void { - const index = this.#connections.indexOf(conn); - if (index !== -1) { - this.#connections.splice(index, 1); - } - } - - // Accepts a new TCP connection and yields all HTTP requests that arrive on - // it. When a connection is accepted, it also creates a new iterator of the - // same kind and adds it to the request multiplexer so that another TCP - // connection can be accepted. - private async *acceptConnAndIterateHttpRequests( - mux: MuxAsyncIterator<ServerRequest>, - ): AsyncIterableIterator<ServerRequest> { - if (this.#closing) return; - // Wait for a new connection. - let conn: Deno.Conn; - try { - conn = await this.listener.accept(); - } catch (error) { - if ( - // The listener is closed: - error instanceof Deno.errors.BadResource || - // TLS handshake errors: - error instanceof Deno.errors.InvalidData || - error instanceof Deno.errors.UnexpectedEof || - error instanceof Deno.errors.ConnectionReset - ) { - return mux.add(this.acceptConnAndIterateHttpRequests(mux)); - } - throw error; - } - this.trackConnection(conn); - // Try to accept another connection and add it to the multiplexer. - mux.add(this.acceptConnAndIterateHttpRequests(mux)); - // Yield the requests that arrive on the just-accepted connection. - yield* this.iterateHttpRequests(conn); - } - - [Symbol.asyncIterator](): AsyncIterableIterator<ServerRequest> { - const mux: MuxAsyncIterator<ServerRequest> = new MuxAsyncIterator(); - mux.add(this.acceptConnAndIterateHttpRequests(mux)); - return mux.iterate(); - } -} - -/** Options for creating an HTTP server. */ -export type HTTPOptions = Omit<Deno.ListenOptions, "transport">; - -/** - * Parse addr from string - * - * const addr = "::1:8000"; - * parseAddrFromString(addr); - * - * @param addr Address string - */ -export function _parseAddrFromStr(addr: string): HTTPOptions { - let url: URL; - try { - const host = addr.startsWith(":") ? `0.0.0.0${addr}` : addr; - url = new URL(`http://${host}`); - } catch { - throw new TypeError("Invalid address."); - } - if ( - url.username || - url.password || - url.pathname != "/" || - url.search || - url.hash - ) { - throw new TypeError("Invalid address."); - } - - return { - hostname: url.hostname, - port: url.port === "" ? 80 : Number(url.port), - }; -} - -/** - * Create a HTTP server - * - * import { serve } from "https://deno.land/std/http/server.ts"; - * const body = "Hello World\n"; - * const server = serve({ port: 8000 }); - * for await (const req of server) { - * req.respond({ body }); - * } - */ -export function serve(addr: string | HTTPOptions): Server { - if (typeof addr === "string") { - addr = _parseAddrFromStr(addr); - } - - const listener = Deno.listen(addr); - return new Server(listener); -} - -/** - * Start an HTTP server with given options and request handler - * - * const body = "Hello World\n"; - * const options = { port: 8000 }; - * listenAndServe(options, (req) => { - * req.respond({ body }); - * }); - * - * @param options Server configuration - * @param handler Request handler - */ -export async function listenAndServe( - addr: string | HTTPOptions, - handler: (req: ServerRequest) => void, -): Promise<void> { - const server = serve(addr); - - for await (const request of server) { - handler(request); - } -} - -/** Options for creating an HTTPS server. */ -export type HTTPSOptions = Omit<Deno.ListenTlsOptions, "transport">; - -/** - * Create an HTTPS server with given options - * - * const body = "Hello HTTPS"; - * const options = { - * hostname: "localhost", - * port: 443, - * certFile: "./path/to/localhost.crt", - * keyFile: "./path/to/localhost.key", - * }; - * for await (const req of serveTLS(options)) { - * req.respond({ body }); - * } - * - * @param options Server configuration - * @return Async iterable server instance for incoming requests - */ -export function serveTLS(options: HTTPSOptions): Server { - const tlsOptions: Deno.ListenTlsOptions = { - ...options, - transport: "tcp", - }; - const listener = Deno.listenTls(tlsOptions); - return new Server(listener); -} - -/** - * Start an HTTPS server with given options and request handler - * - * const body = "Hello HTTPS"; - * const options = { - * hostname: "localhost", - * port: 443, - * certFile: "./path/to/localhost.crt", - * keyFile: "./path/to/localhost.key", - * }; - * listenAndServeTLS(options, (req) => { - * req.respond({ body }); - * }); - * - * @param options Server configuration - * @param handler Request handler - */ -export async function listenAndServeTLS( - options: HTTPSOptions, - handler: (req: ServerRequest) => void, -): Promise<void> { - const server = serveTLS(options); - - for await (const request of server) { - handler(request); - } -} - -/** - * Interface of HTTP server response. - * If body is a Reader, response would be chunked. - * If body is a string, it would be UTF-8 encoded by default. - */ -export interface Response { - status?: number; - headers?: Headers; - body?: Uint8Array | Deno.Reader | string; - trailers?: () => Promise<Headers> | Headers; -} diff --git a/std/http/server_test.ts b/std/http/server_test.ts deleted file mode 100644 index 8a3be71c2..000000000 --- a/std/http/server_test.ts +++ /dev/null @@ -1,784 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Ported from -// https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go - -import { TextProtoReader } from "../textproto/mod.ts"; -import { - assert, - assertEquals, - assertMatch, - assertStringIncludes, - assertThrowsAsync, -} from "../testing/asserts.ts"; -import { - _parseAddrFromStr, - Response, - serve, - Server, - ServerRequest, - serveTLS, -} from "./server.ts"; -import { BufReader, BufWriter } from "../io/bufio.ts"; -import { delay } from "../async/delay.ts"; -import { decode, encode } from "../encoding/utf8.ts"; -import { mockConn } from "./_mock_conn.ts"; -import { dirname, fromFileUrl, join, resolve } from "../path/mod.ts"; - -const moduleDir = dirname(fromFileUrl(import.meta.url)); -const testdataDir = resolve(moduleDir, "testdata"); - -interface ResponseTest { - response: Response; - raw: string; -} - -const responseTests: ResponseTest[] = [ - // Default response - { - response: {}, - raw: "HTTP/1.1 200 OK\r\n" + "content-length: 0" + "\r\n\r\n", - }, - // Empty body with status - { - response: { - status: 404, - }, - raw: "HTTP/1.1 404 Not Found\r\n" + "content-length: 0" + "\r\n\r\n", - }, - // HTTP/1.1, chunked coding; empty trailer; close - { - response: { - status: 200, - body: new Deno.Buffer(new TextEncoder().encode("abcdef")), - }, - - raw: "HTTP/1.1 200 OK\r\n" + - "transfer-encoding: chunked\r\n\r\n" + - "6\r\nabcdef\r\n0\r\n\r\n", - }, -]; - -Deno.test("responseWrite", async function (): Promise<void> { - for (const testCase of responseTests) { - const buf = new Deno.Buffer(); - const bufw = new BufWriter(buf); - const request = new ServerRequest(); - request.w = bufw; - - request.conn = mockConn(); - - await request.respond(testCase.response); - assertEquals(new TextDecoder().decode(buf.bytes()), testCase.raw); - await request.done; - } -}); - -Deno.test("requestContentLength", function (): void { - // Has content length - { - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("content-length", "5"); - const buf = new Deno.Buffer(encode("Hello")); - req.r = new BufReader(buf); - assertEquals(req.contentLength, 5); - } - // No content length - { - const shortText = "Hello"; - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - let chunksData = ""; - let chunkOffset = 0; - const maxChunkSize = 70; - while (chunkOffset < shortText.length) { - const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); - chunksData += `${chunkSize.toString(16)}\r\n${ - shortText.substr(chunkOffset, chunkSize) - }\r\n`; - chunkOffset += chunkSize; - } - chunksData += "0\r\n\r\n"; - const buf = new Deno.Buffer(encode(chunksData)); - req.r = new BufReader(buf); - assertEquals(req.contentLength, null); - } -}); - -interface TotalReader extends Deno.Reader { - total: number; -} -function totalReader(r: Deno.Reader): TotalReader { - let _total = 0; - async function read(p: Uint8Array): Promise<number | null> { - const result = await r.read(p); - if (typeof result === "number") { - _total += result; - } - return result; - } - return { - read, - get total(): number { - return _total; - }, - }; -} -Deno.test("requestBodyWithContentLength", async function (): Promise<void> { - { - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("content-length", "5"); - const buf = new Deno.Buffer(encode("Hello")); - req.r = new BufReader(buf); - const body = decode(await Deno.readAll(req.body)); - assertEquals(body, "Hello"); - } - - // Larger than internal buf - { - const longText = "1234\n".repeat(1000); - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("Content-Length", "5000"); - const buf = new Deno.Buffer(encode(longText)); - req.r = new BufReader(buf); - const body = decode(await Deno.readAll(req.body)); - assertEquals(body, longText); - } - // Handler ignored to consume body -}); -Deno.test( - "ServerRequest.finalize() should consume unread body / content-length", - async () => { - const text = "deno.land"; - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("content-length", "" + text.length); - const tr = totalReader(new Deno.Buffer(encode(text))); - req.r = new BufReader(tr); - req.w = new BufWriter(new Deno.Buffer()); - await req.respond({ status: 200, body: "ok" }); - assertEquals(tr.total, 0); - await req.finalize(); - assertEquals(tr.total, text.length); - }, -); -Deno.test( - "ServerRequest.finalize() should consume unread body / chunked, trailers", - async () => { - const text = [ - "5", - "Hello", - "4", - "Deno", - "0", - "", - "deno: land", - "node: js", - "", - "", - ].join("\r\n"); - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - req.headers.set("trailer", "deno,node"); - const body = encode(text); - const tr = totalReader(new Deno.Buffer(body)); - req.r = new BufReader(tr); - req.w = new BufWriter(new Deno.Buffer()); - await req.respond({ status: 200, body: "ok" }); - assertEquals(tr.total, 0); - assertEquals(req.headers.has("trailer"), true); - assertEquals(req.headers.has("deno"), false); - assertEquals(req.headers.has("node"), false); - await req.finalize(); - assertEquals(tr.total, body.byteLength); - assertEquals(req.headers.has("trailer"), false); - assertEquals(req.headers.get("deno"), "land"); - assertEquals(req.headers.get("node"), "js"); - }, -); -Deno.test("requestBodyWithTransferEncoding", async function (): Promise<void> { - { - const shortText = "Hello"; - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - let chunksData = ""; - let chunkOffset = 0; - const maxChunkSize = 70; - while (chunkOffset < shortText.length) { - const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); - chunksData += `${chunkSize.toString(16)}\r\n${ - shortText.substr(chunkOffset, chunkSize) - }\r\n`; - chunkOffset += chunkSize; - } - chunksData += "0\r\n\r\n"; - const buf = new Deno.Buffer(encode(chunksData)); - req.r = new BufReader(buf); - const body = decode(await Deno.readAll(req.body)); - assertEquals(body, shortText); - } - - // Larger than internal buf - { - const longText = "1234\n".repeat(1000); - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - let chunksData = ""; - let chunkOffset = 0; - const maxChunkSize = 70; - while (chunkOffset < longText.length) { - const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset); - chunksData += `${chunkSize.toString(16)}\r\n${ - longText.substr(chunkOffset, chunkSize) - }\r\n`; - chunkOffset += chunkSize; - } - chunksData += "0\r\n\r\n"; - const buf = new Deno.Buffer(encode(chunksData)); - req.r = new BufReader(buf); - const body = decode(await Deno.readAll(req.body)); - assertEquals(body, longText); - } -}); - -Deno.test("requestBodyReaderWithContentLength", async function (): Promise< - void -> { - { - const shortText = "Hello"; - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("content-length", "" + shortText.length); - const buf = new Deno.Buffer(encode(shortText)); - req.r = new BufReader(buf); - const readBuf = new Uint8Array(6); - let offset = 0; - while (offset < shortText.length) { - const nread = await req.body.read(readBuf); - assert(nread !== null); - const s = decode(readBuf.subarray(0, nread as number)); - assertEquals(shortText.substr(offset, nread as number), s); - offset += nread as number; - } - const nread = await req.body.read(readBuf); - assertEquals(nread, null); - } - - // Larger than given buf - { - const longText = "1234\n".repeat(1000); - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("Content-Length", "5000"); - const buf = new Deno.Buffer(encode(longText)); - req.r = new BufReader(buf); - const readBuf = new Uint8Array(1000); - let offset = 0; - while (offset < longText.length) { - const nread = await req.body.read(readBuf); - assert(nread !== null); - const s = decode(readBuf.subarray(0, nread as number)); - assertEquals(longText.substr(offset, nread as number), s); - offset += nread as number; - } - const nread = await req.body.read(readBuf); - assertEquals(nread, null); - } -}); - -Deno.test("requestBodyReaderWithTransferEncoding", async function (): Promise< - void -> { - { - const shortText = "Hello"; - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - let chunksData = ""; - let chunkOffset = 0; - const maxChunkSize = 70; - while (chunkOffset < shortText.length) { - const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); - chunksData += `${chunkSize.toString(16)}\r\n${ - shortText.substr(chunkOffset, chunkSize) - }\r\n`; - chunkOffset += chunkSize; - } - chunksData += "0\r\n\r\n"; - const buf = new Deno.Buffer(encode(chunksData)); - req.r = new BufReader(buf); - const readBuf = new Uint8Array(6); - let offset = 0; - while (offset < shortText.length) { - const nread = await req.body.read(readBuf); - assert(nread !== null); - const s = decode(readBuf.subarray(0, nread as number)); - assertEquals(shortText.substr(offset, nread as number), s); - offset += nread as number; - } - const nread = await req.body.read(readBuf); - assertEquals(nread, null); - } - - // Larger than internal buf - { - const longText = "1234\n".repeat(1000); - const req = new ServerRequest(); - req.headers = new Headers(); - req.headers.set("transfer-encoding", "chunked"); - let chunksData = ""; - let chunkOffset = 0; - const maxChunkSize = 70; - while (chunkOffset < longText.length) { - const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset); - chunksData += `${chunkSize.toString(16)}\r\n${ - longText.substr(chunkOffset, chunkSize) - }\r\n`; - chunkOffset += chunkSize; - } - chunksData += "0\r\n\r\n"; - const buf = new Deno.Buffer(encode(chunksData)); - req.r = new BufReader(buf); - const readBuf = new Uint8Array(1000); - let offset = 0; - while (offset < longText.length) { - const nread = await req.body.read(readBuf); - assert(nread !== null); - const s = decode(readBuf.subarray(0, nread as number)); - assertEquals(longText.substr(offset, nread as number), s); - offset += nread as number; - } - const nread = await req.body.read(readBuf); - assertEquals(nread, null); - } -}); - -Deno.test({ - name: "destroyed connection", - fn: async (): Promise<void> => { - // Runs a simple server as another process - const p = Deno.run({ - cmd: [ - Deno.execPath(), - "run", - "--quiet", - "--allow-net", - "testdata/simple_server.ts", - ], - cwd: moduleDir, - stdout: "piped", - }); - - let serverIsRunning = true; - const statusPromise = p - .status() - .then((): void => { - serverIsRunning = false; - }) - .catch((_): void => {}); // Ignores the error when closing the process. - - try { - const r = new TextProtoReader(new BufReader(p.stdout)); - const s = await r.readLine(); - assert(s !== null && s.includes("server listening")); - await delay(100); - // Reqeusts to the server and immediately closes the connection - const conn = await Deno.connect({ port: 4502 }); - await conn.write(new TextEncoder().encode("GET / HTTP/1.0\n\n")); - conn.close(); - // Waits for the server to handle the above (broken) request - await delay(100); - assert(serverIsRunning); - } finally { - // Stops the sever and allows `p.status()` promise to resolve - Deno.kill(p.pid, Deno.Signal.SIGKILL); - await statusPromise; - p.stdout.close(); - p.close(); - } - }, -}); - -Deno.test({ - name: "serveTLS", - fn: async (): Promise<void> => { - // Runs a simple server as another process - const p = Deno.run({ - cmd: [ - Deno.execPath(), - "run", - "--quiet", - "--allow-net", - "--allow-read", - "testdata/simple_https_server.ts", - ], - cwd: moduleDir, - stdout: "piped", - }); - - let serverIsRunning = true; - const statusPromise = p - .status() - .then((): void => { - serverIsRunning = false; - }) - .catch((_): void => {}); // Ignores the error when closing the process. - - try { - const r = new TextProtoReader(new BufReader(p.stdout)); - const s = await r.readLine(); - assert( - s !== null && s.includes("server listening"), - "server must be started", - ); - // Requests to the server and immediately closes the connection - const conn = await Deno.connectTls({ - hostname: "localhost", - port: 4503, - certFile: join(testdataDir, "tls/RootCA.pem"), - }); - await Deno.writeAll( - conn, - new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"), - ); - const res = new Uint8Array(100); - const nread = await conn.read(res); - assert(nread !== null); - conn.close(); - const resStr = new TextDecoder().decode(res.subarray(0, nread)); - assert(resStr.includes("Hello HTTPS")); - assert(serverIsRunning); - } finally { - // Stops the sever and allows `p.status()` promise to resolve - Deno.kill(p.pid, Deno.Signal.SIGKILL); - await statusPromise; - p.stdout.close(); - p.close(); - } - }, -}); - -Deno.test( - "close server while iterating", - async (): Promise<void> => { - const server = serve(":8123"); - const nextWhileClosing = server[Symbol.asyncIterator]().next(); - server.close(); - assertEquals(await nextWhileClosing, { value: undefined, done: true }); - - const nextAfterClosing = server[Symbol.asyncIterator]().next(); - assertEquals(await nextAfterClosing, { value: undefined, done: true }); - }, -); - -Deno.test({ - name: "[http] close server while connection is open", - async fn(): Promise<void> { - async function iteratorReq(server: Server): Promise<void> { - for await (const req of server) { - await req.respond({ body: new TextEncoder().encode(req.url) }); - } - } - - const server = serve(":8123"); - const p = iteratorReq(server); - const conn = await Deno.connect({ hostname: "127.0.0.1", port: 8123 }); - await Deno.writeAll( - conn, - new TextEncoder().encode("GET /hello HTTP/1.1\r\n\r\n"), - ); - const res = new Uint8Array(100); - const nread = await conn.read(res); - assert(nread !== null); - const resStr = new TextDecoder().decode(res.subarray(0, nread)); - assertStringIncludes(resStr, "/hello"); - server.close(); - await p; - // Client connection should still be open, verify that - // it's visible in resource table. - const resources = Deno.resources(); - assertEquals(resources[conn.rid], "tcpStream"); - conn.close(); - }, -}); - -Deno.test({ - name: "respond error closes connection", - async fn(): Promise<void> { - const serverRoutine = async (): Promise<void> => { - const server = serve(":8124"); - for await (const req of server) { - await assertThrowsAsync(async () => { - await req.respond({ - status: 12345, - body: new TextEncoder().encode("Hello World"), - }); - }, Deno.errors.InvalidData); - // The connection should be destroyed - assert(!(req.conn.rid in Deno.resources())); - server.close(); - } - }; - const p = serverRoutine(); - const conn = await Deno.connect({ - hostname: "127.0.0.1", - port: 8124, - }); - await Deno.writeAll( - conn, - new TextEncoder().encode("GET / HTTP/1.1\r\n\r\n"), - ); - conn.close(); - await p; - }, -}); - -Deno.test({ - name: "[http] request error gets 400 response", - async fn(): Promise<void> { - const server = serve(":8124"); - const entry = server[Symbol.asyncIterator]().next(); - const conn = await Deno.connect({ - hostname: "127.0.0.1", - port: 8124, - }); - await Deno.writeAll( - conn, - encode("GET / HTTP/1.1\r\nmalformedHeader\r\n\r\n\r\n\r\n"), - ); - const responseString = decode(await Deno.readAll(conn)); - assertMatch( - responseString, - /^HTTP\/1\.1 400 Bad Request\r\ncontent-length: \d+\r\n\r\n.*\r\n\r\n$/ms, - ); - conn.close(); - server.close(); - assert((await entry).done); - }, -}); - -Deno.test({ - name: "[http] finalizing invalid chunked data closes connection", - async fn(): Promise<void> { - const serverRoutine = async (): Promise<void> => { - const server = serve(":8124"); - for await (const req of server) { - await req.respond({ status: 200, body: "Hello, world!" }); - break; - } - server.close(); - }; - const p = serverRoutine(); - const conn = await Deno.connect({ - hostname: "127.0.0.1", - port: 8124, - }); - await Deno.writeAll( - conn, - encode( - "PUT / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nzzzzzzz\r\nhello", - ), - ); - await conn.closeWrite(); - const responseString = decode(await Deno.readAll(conn)); - assertEquals( - responseString, - "HTTP/1.1 200 OK\r\ncontent-length: 13\r\n\r\nHello, world!", - ); - conn.close(); - await p; - }, -}); - -Deno.test({ - name: "[http] finalizing chunked unexpected EOF closes connection", - async fn(): Promise<void> { - const serverRoutine = async (): Promise<void> => { - const server = serve(":8124"); - for await (const req of server) { - await req.respond({ status: 200, body: "Hello, world!" }); - break; - } - server.close(); - }; - const p = serverRoutine(); - const conn = await Deno.connect({ - hostname: "127.0.0.1", - port: 8124, - }); - await Deno.writeAll( - conn, - encode("PUT / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello"), - ); - conn.closeWrite(); - const responseString = decode(await Deno.readAll(conn)); - assertEquals( - responseString, - "HTTP/1.1 200 OK\r\ncontent-length: 13\r\n\r\nHello, world!", - ); - conn.close(); - await p; - }, -}); - -Deno.test({ - name: - "[http] receiving bad request from a closed connection should not throw", - async fn(): Promise<void> { - const server = serve(":8124"); - const serverRoutine = async (): Promise<void> => { - for await (const req of server) { - await req.respond({ status: 200, body: "Hello, world!" }); - } - }; - const p = serverRoutine(); - const conn = await Deno.connect({ - hostname: "127.0.0.1", - port: 8124, - }); - await Deno.writeAll( - conn, - encode([ - // A normal request is required: - "GET / HTTP/1.1", - "Host: localhost", - "", - // The bad request: - "GET / HTTP/1.1", - "Host: localhost", - "INVALID!HEADER!", - "", - "", - ].join("\r\n")), - ); - // After sending the two requests, don't receive the reponses. - - // Closing the connection now. - conn.close(); - - // The server will write responses to the closed connection, - // the first few `write()` calls will not throws, until the server received - // the TCP RST. So we need the normal request before the bad request to - // make the server do a few writes before it writes that `400` response. - - // Wait for server to handle requests. - await delay(10); - - server.close(); - await p; - }, -}); - -Deno.test({ - name: "serveTLS Invalid Cert", - fn: async (): Promise<void> => { - async function iteratorReq(server: Server): Promise<void> { - for await (const req of server) { - await req.respond({ body: new TextEncoder().encode("Hello HTTPS") }); - } - } - const port = 9122; - const tlsOptions = { - hostname: "localhost", - port, - certFile: join(testdataDir, "tls/localhost.crt"), - keyFile: join(testdataDir, "tls/localhost.key"), - }; - const server = serveTLS(tlsOptions); - const p = iteratorReq(server); - - try { - // Invalid certificate, connection should throw - // but should not crash the server - assertThrowsAsync( - () => - Deno.connectTls({ - hostname: "localhost", - port, - // certFile - }), - Deno.errors.InvalidData, - ); - - // Valid request after invalid - const conn = await Deno.connectTls({ - hostname: "localhost", - port, - certFile: join(testdataDir, "tls/RootCA.pem"), - }); - - await Deno.writeAll( - conn, - new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"), - ); - const res = new Uint8Array(100); - const nread = await conn.read(res); - assert(nread !== null); - conn.close(); - const resStr = new TextDecoder().decode(res.subarray(0, nread)); - assert(resStr.includes("Hello HTTPS")); - } finally { - // Stops the sever and allows `p.status()` promise to resolve - server.close(); - await p; - } - }, -}); - -Deno.test({ - name: "server.serve() should be able to parse IPV4 address", - fn: (): void => { - const server = serve("127.0.0.1:8124"); - const expected = { - hostname: "127.0.0.1", - port: 8124, - transport: "tcp", - }; - assertEquals(server.listener.addr, expected); - server.close(); - }, -}); - -Deno.test({ - name: "server._parseAddrFromStr() should be able to parse IPV6 address", - fn: (): void => { - const addr = _parseAddrFromStr("[::1]:8124"); - const expected = { - hostname: "[::1]", - port: 8124, - }; - assertEquals(addr, expected); - }, -}); - -Deno.test({ - name: "server.serve() should be able to parse IPV6 address", - fn: (): void => { - const server = serve("[::1]:8124"); - const expected = { - hostname: "::1", - port: 8124, - transport: "tcp", - }; - assertEquals(server.listener.addr, expected); - server.close(); - }, -}); - -Deno.test({ - name: "server._parseAddrFromStr() port 80", - fn: (): void => { - const addr = _parseAddrFromStr(":80"); - assertEquals(addr.port, 80); - assertEquals(addr.hostname, "0.0.0.0"); - }, -}); diff --git a/std/http/test.ts b/std/http/test.ts deleted file mode 100644 index 590417055..000000000 --- a/std/http/test.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import "./mod.ts"; diff --git a/std/http/testdata/% b/std/http/testdata/% deleted file mode 100644 index e69de29bb..000000000 --- a/std/http/testdata/% +++ /dev/null diff --git a/std/http/testdata/file_server_as_library.ts b/std/http/testdata/file_server_as_library.ts deleted file mode 100644 index cd4bf68db..000000000 --- a/std/http/testdata/file_server_as_library.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { serve } from "../server.ts"; -import { serveFile } from "../file_server.ts"; - -const server = serve({ port: 8000 }); - -console.log("Server running..."); - -for await (const req of server) { - serveFile(req, "./testdata/hello.html").then((response) => { - req.respond(response); - }); -} diff --git a/std/http/testdata/hello.html b/std/http/testdata/hello.html deleted file mode 100644 index e69de29bb..000000000 --- a/std/http/testdata/hello.html +++ /dev/null diff --git a/std/http/testdata/simple_https_server.ts b/std/http/testdata/simple_https_server.ts deleted file mode 100644 index 84dfb39ab..000000000 --- a/std/http/testdata/simple_https_server.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -// This is an example of a https server -import { serveTLS } from "../server.ts"; - -const tlsOptions = { - hostname: "localhost", - port: 4503, - certFile: "./testdata/tls/localhost.crt", - keyFile: "./testdata/tls/localhost.key", -}; -const s = serveTLS(tlsOptions); -console.log( - `Simple HTTPS server listening on ${tlsOptions.hostname}:${tlsOptions.port}`, -); -const body = new TextEncoder().encode("Hello HTTPS"); -for await (const req of s) { - req.respond({ body }); -} diff --git a/std/http/testdata/simple_server.ts b/std/http/testdata/simple_server.ts deleted file mode 100644 index ff2a0b5ac..000000000 --- a/std/http/testdata/simple_server.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -// This is an example of a server that responds with an empty body -import { serve } from "../server.ts"; - -const addr = "0.0.0.0:4502"; -console.log(`Simple server listening on ${addr}`); -for await (const req of serve(addr)) { - req.respond({}); -} diff --git a/std/http/testdata/test file.txt b/std/http/testdata/test file.txt deleted file mode 100644 index e69de29bb..000000000 --- a/std/http/testdata/test file.txt +++ /dev/null diff --git a/std/http/testdata/tls b/std/http/testdata/tls deleted file mode 120000 index f6fd22ed8..000000000 --- a/std/http/testdata/tls +++ /dev/null @@ -1 +0,0 @@ -../../../cli/tests/tls
\ No newline at end of file |