diff --git a/std/http/cookie_test.ts b/std/http/cookie_test.ts index 5e29fede73..8ab862bb3a 100644 --- a/std/http/cookie_test.ts +++ b/std/http/cookie_test.ts @@ -2,9 +2,10 @@ import { ServerRequest, Response } from "./server.ts"; import { getCookies, delCookie, setCookie } from "./cookie.ts"; import { assert, assertEquals } from "../testing/asserts.ts"; +const { test } = Deno; -Deno.test({ - name: "[HTTP] Cookie parser", +test({ + name: "Cookie parser", fn(): void { const req = new ServerRequest(); req.headers = new Headers(); @@ -31,8 +32,8 @@ Deno.test({ } }); -Deno.test({ - name: "[HTTP] Cookie Delete", +test({ + name: "Cookie Delete", fn(): void { const res: Response = {}; delCookie(res, "deno"); @@ -43,8 +44,8 @@ Deno.test({ } }); -Deno.test({ - name: "[HTTP] Cookie Set", +test({ + name: "Cookie Set", fn(): void { const res: Response = {}; diff --git a/std/http/file_server.ts b/std/http/file_server.ts index da2b0b5a2b..e1fe14fcfb 100755 --- a/std/http/file_server.ts +++ b/std/http/file_server.ts @@ -8,14 +8,10 @@ const { args, stat, readDir, open, exit } = Deno; import { posix } from "../path/mod.ts"; -import { - listenAndServe, - ServerRequest, - setContentLength, - Response -} from "./server.ts"; +import { listenAndServe, ServerRequest, Response } from "./server.ts"; import { parse } from "../flags/mod.ts"; import { assert } from "../testing/asserts.ts"; +import { setContentLength } from "./io.ts"; interface EntryInfo { mode: string; diff --git a/std/http/file_server_test.ts b/std/http/file_server_test.ts index 7c60d5f562..a023aca3d2 100644 --- a/std/http/file_server_test.ts +++ b/std/http/file_server_test.ts @@ -3,7 +3,6 @@ import { assert, assertEquals, assertStrContains } from "../testing/asserts.ts"; import { BufReader } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; const { test } = Deno; - let fileServer: Deno.Process; async function startFileServer(): Promise { diff --git a/std/http/io.ts b/std/http/io.ts index a51fada54b..477bab8314 100644 --- a/std/http/io.ts +++ b/std/http/io.ts @@ -2,6 +2,8 @@ import { BufReader, UnexpectedEOFError, BufWriter } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; import { assert } from "../testing/asserts.ts"; import { encoder } from "../strings/mod.ts"; +import { ServerRequest, Response } from "./server.ts"; +import { STATUS_TEXT } from "./http_status.ts"; export function emptyReader(): Deno.Reader { return { @@ -211,3 +213,176 @@ export async function writeTrailers( await writer.write(encoder.encode("\r\n")); await writer.flush(); } + +export function setContentLength(r: Response): void { + if (!r.headers) { + r.headers = new Headers(); + } + + if (r.body) { + if (!r.headers.has("content-length")) { + // typeof r.body === "string" handled in writeResponse. + if (r.body instanceof Uint8Array) { + const bodyLength = r.body.byteLength; + r.headers.set("content-length", bodyLength.toString()); + } else { + r.headers.set("transfer-encoding", "chunked"); + } + } + } +} + +export async function writeResponse( + w: Deno.Writer, + r: Response +): Promise { + const protoMajor = 1; + const protoMinor = 1; + const statusCode = r.status || 200; + const statusText = STATUS_TEXT.get(statusCode); + const writer = BufWriter.create(w); + if (!statusText) { + throw Error("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`; + + setContentLength(r); + assert(r.headers != null); + const headers = r.headers; + + 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(writer, r.body); + 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, true). + * 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 + const digitReg = /^\d+$/; // test if string is only digit + + if (!vers.startsWith("HTTP/")) { + break; + } + + const dot = vers.indexOf("."); + if (dot < 0) { + break; + } + + const majorStr = vers.substring(vers.indexOf("/") + 1, dot); + const major = parseInt(majorStr); + if ( + !digitReg.test(majorStr) || + isNaN(major) || + major < 0 || + major > Big + ) { + break; + } + + const minorStr = vers.substring(dot + 1); + const minor = parseInt(minorStr); + if ( + !digitReg.test(minorStr) || + isNaN(minor) || + minor < 0 || + minor > Big + ) { + break; + } + + return [major, minor]; + } + } + + throw new Error(`malformed HTTP version ${vers}`); +} + +export async function readRequest( + conn: Deno.Conn, + bufr: BufReader +): Promise { + const tp = new TextProtoReader(bufr); + const firstLine = await tp.readLine(); // e.g. GET /index.html HTTP/1.0 + if (firstLine === Deno.EOF) return Deno.EOF; + const headers = await tp.readMIMEHeader(); + if (headers === Deno.EOF) throw new UnexpectedEOFError(); + + const req = new ServerRequest(); + req.conn = conn; + req.r = bufr; + [req.method, req.url, req.proto] = firstLine.split(" ", 3); + [req.protoMinor, req.protoMajor] = parseHTTPVersion(req.proto); + req.headers = headers; + fixLength(req); + return req; +} + +function fixLength(req: ServerRequest): void { + const contentLength = req.headers.get("Content-Length"); + if (contentLength) { + const arrClen = contentLength.split(","); + if (arrClen.length > 1) { + const distinct = [...new Set(arrClen.map((e): string => e.trim()))]; + if (distinct.length > 1) { + throw Error("cannot contain multiple Content-Length headers"); + } else { + req.headers.set("Content-Length", distinct[0]); + } + } + const c = req.headers.get("Content-Length"); + if (req.method === "HEAD" && c && c !== "0") { + throw Error("http: method cannot contain a Content-Length"); + } + if (c && req.headers.has("transfer-encoding")) { + // A sender MUST NOT send a Content-Length header field in any message + // that contains a Transfer-Encoding header field. + // rfc: https://tools.ietf.org/html/rfc7230#section-3.3.2 + throw new Error( + "http: Transfer-Encoding and Content-Length cannot be send together" + ); + } + } +} diff --git a/std/http/io_test.ts b/std/http/io_test.ts index 7e77015963..acaa882f74 100644 --- a/std/http/io_test.ts +++ b/std/http/io_test.ts @@ -1,13 +1,26 @@ import { AssertionError, assertThrowsAsync, - assertEquals + assertEquals, + assert, + assertNotEOF, + assertNotEquals } from "../testing/asserts.ts"; -import { bodyReader, writeTrailers, readTrailers } from "./io.ts"; +import { + bodyReader, + writeTrailers, + readTrailers, + parseHTTPVersion, + readRequest, + writeResponse +} from "./io.ts"; import { encode, decode } from "../strings/mod.ts"; -import { BufReader } from "../io/bufio.ts"; +import { BufReader, UnexpectedEOFError, ReadLineResult } from "../io/bufio.ts"; import { chunkedBodyReader } from "./io.ts"; -const { test, Buffer } = Deno; +import { ServerRequest, Response } from "./server.ts"; +import { StringReader } from "../io/readers.ts"; +import { mockConn } from "./mock.ts"; +const { Buffer, test } = Deno; test("bodyReader", async () => { const text = "Hello, Deno"; @@ -165,3 +178,274 @@ test("writeTrailer should throw", async () => { "Not trailer" ); }); + +// Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request_test.go#L535-L565 +test("parseHttpVersion", (): void => { + const testCases = [ + { in: "HTTP/0.9", want: [0, 9] }, + { in: "HTTP/1.0", want: [1, 0] }, + { in: "HTTP/1.1", want: [1, 1] }, + { in: "HTTP/3.14", want: [3, 14] }, + { in: "HTTP", err: true }, + { in: "HTTP/one.one", err: true }, + { in: "HTTP/1.1/", err: true }, + { in: "HTTP/-1.0", err: true }, + { in: "HTTP/0.-1", err: true }, + { in: "HTTP/", err: true }, + { in: "HTTP/1,0", err: true } + ]; + for (const t of testCases) { + let r, err; + try { + r = parseHTTPVersion(t.in); + } catch (e) { + err = e; + } + if (t.err) { + assert(err instanceof Error, t.in); + } else { + assertEquals(err, undefined); + assertEquals(r, t.want, t.in); + } + } +}); + +test(async function writeUint8ArrayResponse(): Promise { + const shortText = "Hello"; + + const body = new TextEncoder().encode(shortText); + const res: Response = { body }; + + const buf = new Deno.Buffer(); + await writeResponse(buf, res); + + const decoder = new TextDecoder("utf-8"); + const reader = new BufReader(buf); + + let r: ReadLineResult; + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), `content-length: ${shortText.length}`); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(r.line.byteLength, 0); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), shortText); + assertEquals(r.more, false); + + const eof = await reader.readLine(); + assertEquals(eof, Deno.EOF); +}); + +test(async function writeStringResponse(): Promise { + const body = "Hello"; + + const res: Response = { body }; + + const buf = new Deno.Buffer(); + await writeResponse(buf, res); + + const decoder = new TextDecoder("utf-8"); + const reader = new BufReader(buf); + + let r: ReadLineResult; + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), `content-length: ${body.length}`); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(r.line.byteLength, 0); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), body); + assertEquals(r.more, false); + + const eof = await reader.readLine(); + assertEquals(eof, Deno.EOF); +}); + +test(async function writeStringReaderResponse(): Promise { + const shortText = "Hello"; + + const body = new StringReader(shortText); + const res: Response = { body }; + + const buf = new Deno.Buffer(); + await writeResponse(buf, res); + + const decoder = new TextDecoder("utf-8"); + const reader = new BufReader(buf); + + let r: ReadLineResult; + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), "transfer-encoding: chunked"); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(r.line.byteLength, 0); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), shortText.length.toString()); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), shortText); + assertEquals(r.more, false); + + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), "0"); + assertEquals(r.more, false); +}); + +test("writeResponse with trailer", async () => { + const w = new Buffer(); + const body = new StringReader("Hello"); + await writeResponse(w, { + status: 200, + headers: new Headers({ + "transfer-encoding": "chunked", + trailer: "deno,node" + }), + body, + trailers: () => new Headers({ deno: "land", node: "js" }) + }); + const ret = w.toString(); + const exp = [ + "HTTP/1.1 200 OK", + "transfer-encoding: chunked", + "trailer: deno,node", + "", + "5", + "Hello", + "0", + "", + "deno: land", + "node: js", + "", + "" + ].join("\r\n"); + assertEquals(ret, exp); +}); + +test(async function readRequestError(): Promise { + const input = `GET / HTTP/1.1 +malformedHeader +`; + const reader = new BufReader(new StringReader(input)); + let err; + try { + await readRequest(mockConn(), reader); + } catch (e) { + err = e; + } + assert(err instanceof Error); + assertEquals(err.message, "malformed MIME header line: malformedHeader"); +}); + +// Ported from Go +// https://github.com/golang/go/blob/go1.12.5/src/net/http/request_test.go#L377-L443 +// TODO(zekth) fix tests +test(async function testReadRequestError(): Promise { + const testCases = [ + { + in: "GET / HTTP/1.1\r\nheader: foo\r\n\r\n", + headers: [{ key: "header", value: "foo" }] + }, + { + in: "GET / HTTP/1.1\r\nheader:foo\r\n", + err: UnexpectedEOFError + }, + { in: "", err: Deno.EOF }, + { + 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 | Deno.EOF | undefined; + try { + req = await readRequest(mockConn(), reader); + } catch (e) { + err = e; + } + if (test.err === Deno.EOF) { + assertEquals(req, Deno.EOF); + } else if (typeof test.err === "string") { + assertEquals(err.message, test.err); + } else if (test.err) { + assert(err instanceof (test.err as typeof UnexpectedEOFError)); + } else { + assert(req instanceof ServerRequest); + assert(test.headers); + assertEquals(err, undefined); + assertNotEquals(req, Deno.EOF); + for (const h of test.headers) { + assertEquals(req.headers.get(h.key), h.value); + } + } + } +}); diff --git a/std/http/mock.ts b/std/http/mock.ts new file mode 100644 index 0000000000..3a4eeed821 --- /dev/null +++ b/std/http/mock.ts @@ -0,0 +1,26 @@ +/** Create dummy Deno.Conn object with given base properties */ +export function mockConn(base: Partial = {}): Deno.Conn { + return { + localAddr: { + transport: "tcp", + hostname: "", + port: 0 + }, + remoteAddr: { + transport: "tcp", + hostname: "", + port: 0 + }, + rid: -1, + closeRead: (): void => {}, + closeWrite: (): void => {}, + read: async (): Promise => { + return 0; + }, + write: async (): Promise => { + return -1; + }, + close: (): void => {}, + ...base + }; +} diff --git a/std/http/racing_server_test.ts b/std/http/racing_server_test.ts index 07df92baef..0f228419d4 100644 --- a/std/http/racing_server_test.ts +++ b/std/http/racing_server_test.ts @@ -1,8 +1,7 @@ -const { connect, run } = Deno; - import { assert, assertEquals } from "../testing/asserts.ts"; import { BufReader, BufWriter } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; +const { connect, run, test } = Deno; let server: Deno.Process; async function startServer(): Promise { @@ -59,7 +58,7 @@ content-length: 6 Step7 `; -Deno.test(async function serverPipelineRace(): Promise { +test(async function serverPipelineRace(): Promise { await startServer(); const conn = await connect({ port: 4501 }); diff --git a/std/http/server.ts b/std/http/server.ts index e7d6bd5985..6e26e84564 100644 --- a/std/http/server.ts +++ b/std/http/server.ts @@ -1,91 +1,19 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -const { listen, listenTLS, copy } = Deno; -type Listener = Deno.Listener; -type Conn = Deno.Conn; -type Reader = Deno.Reader; -type Writer = Deno.Writer; -import { BufReader, BufWriter, UnexpectedEOFError } from "../io/bufio.ts"; -import { TextProtoReader } from "../textproto/mod.ts"; -import { STATUS_TEXT } from "./http_status.ts"; +import { BufReader, BufWriter } from "../io/bufio.ts"; import { assert } from "../testing/asserts.ts"; import { deferred, Deferred, MuxAsyncIterator } from "../util/async.ts"; import { bodyReader, chunkedBodyReader, - writeChunkedBody, - writeTrailers, - emptyReader + emptyReader, + writeResponse, + readRequest } from "./io.ts"; - -const encoder = new TextEncoder(); - -export function setContentLength(r: Response): void { - if (!r.headers) { - r.headers = new Headers(); - } - - if (r.body) { - if (!r.headers.has("content-length")) { - // typeof r.body === "string" handled in writeResponse. - if (r.body instanceof Uint8Array) { - const bodyLength = r.body.byteLength; - r.headers.set("content-length", bodyLength.toString()); - } else { - r.headers.set("transfer-encoding", "chunked"); - } - } - } -} - -export async function writeResponse(w: Writer, r: Response): Promise { - const protoMajor = 1; - const protoMinor = 1; - const statusCode = r.status || 200; - const statusText = STATUS_TEXT.get(statusCode); - const writer = BufWriter.create(w); - if (!statusText) { - throw Error("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`; - - setContentLength(r); - assert(r.headers != null); - const headers = r.headers; - - 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 copy(writer, r.body); - 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(); -} +import { encode } from "../strings/mod.ts"; +import Listener = Deno.Listener; +import Conn = Deno.Conn; +import Reader = Deno.Reader; +const { listen, listenTLS } = Deno; export class ServerRequest { url!: string; @@ -194,108 +122,6 @@ export class ServerRequest { } } -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" - ); - } - } -} - -/** - * ParseHTTPVersion parses a HTTP version string. - * "HTTP/1.0" returns (1, 0, true). - * 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 - const digitReg = /^\d+$/; // test if string is only digit - - if (!vers.startsWith("HTTP/")) { - break; - } - - const dot = vers.indexOf("."); - if (dot < 0) { - break; - } - - const majorStr = vers.substring(vers.indexOf("/") + 1, dot); - const major = parseInt(majorStr); - if ( - !digitReg.test(majorStr) || - isNaN(major) || - major < 0 || - major > Big - ) { - break; - } - - const minorStr = vers.substring(dot + 1); - const minor = parseInt(minorStr); - if ( - !digitReg.test(minorStr) || - isNaN(minor) || - minor < 0 || - minor > Big - ) { - break; - } - - return [major, minor]; - } - } - - throw new Error(`malformed HTTP version ${vers}`); -} - -export async function readRequest( - conn: Conn, - bufr: BufReader -): Promise { - const tp = new TextProtoReader(bufr); - const firstLine = await tp.readLine(); // e.g. GET /index.html HTTP/1.0 - if (firstLine === Deno.EOF) return Deno.EOF; - const headers = await tp.readMIMEHeader(); - if (headers === Deno.EOF) throw new UnexpectedEOFError(); - - const req = new ServerRequest(); - req.conn = conn; - req.r = bufr; - [req.method, req.url, req.proto] = firstLine.split(" ", 3); - [req.protoMinor, req.protoMajor] = parseHTTPVersion(req.proto); - req.headers = headers; - fixLength(req); - return req; -} - export class Server implements AsyncIterable { private closing = false; @@ -349,7 +175,7 @@ export class Server implements AsyncIterable { try { await writeResponse(req.w, { status: 400, - body: encoder.encode(`${err.message}\r\n\r\n`) + body: encode(`${err.message}\r\n\r\n`) }); } catch (_) { // The connection is destroyed. diff --git a/std/http/server_test.ts b/std/http/server_test.ts index 70ce5f2f17..fec4879250 100644 --- a/std/http/server_test.ts +++ b/std/http/server_test.ts @@ -5,65 +5,21 @@ // Ported from // https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go -const { Buffer, test } = Deno; import { TextProtoReader } from "../textproto/mod.ts"; -import { - assert, - assertEquals, - assertNotEquals, - assertNotEOF -} from "../testing/asserts.ts"; -import { - Response, - ServerRequest, - writeResponse, - serve, - readRequest, - parseHTTPVersion -} from "./server.ts"; -import { - BufReader, - BufWriter, - ReadLineResult, - UnexpectedEOFError -} from "../io/bufio.ts"; +import { assert, assertEquals, assertNotEOF } from "../testing/asserts.ts"; +import { Response, ServerRequest, serve } from "./server.ts"; +import { BufReader, BufWriter } from "../io/bufio.ts"; import { delay, deferred } from "../util/async.ts"; -import { StringReader } from "../io/readers.ts"; -import { encode } from "../strings/mod.ts"; +import { encode, decode } from "../strings/mod.ts"; +import { mockConn } from "./mock.ts"; + +const { Buffer, test } = Deno; interface ResponseTest { response: Response; raw: string; } -const enc = new TextEncoder(); -const dec = new TextDecoder(); - -type Handler = () => void; - -const mockConn = (): Deno.Conn => ({ - localAddr: { - transport: "tcp", - hostname: "", - port: 0 - }, - remoteAddr: { - transport: "tcp", - hostname: "", - port: 0 - }, - rid: -1, - closeRead: (): void => {}, - closeWrite: (): void => {}, - read: async (): Promise => { - return 0; - }, - write: async (): Promise => { - return -1; - }, - close: (): void => {} -}); - const responseTests: ResponseTest[] = [ // Default response { @@ -112,7 +68,7 @@ test(async function requestContentLength(): Promise { const req = new ServerRequest(); req.headers = new Headers(); req.headers.set("content-length", "5"); - const buf = new Buffer(enc.encode("Hello")); + const buf = new Buffer(encode("Hello")); req.r = new BufReader(buf); assertEquals(req.contentLength, 5); } @@ -134,7 +90,7 @@ test(async function requestContentLength(): Promise { chunkOffset += chunkSize; } chunksData += "0\r\n\r\n"; - const buf = new Buffer(enc.encode(chunksData)); + const buf = new Buffer(encode(chunksData)); req.r = new BufReader(buf); assertEquals(req.contentLength, null); } @@ -164,9 +120,9 @@ test(async function requestBodyWithContentLength(): Promise { const req = new ServerRequest(); req.headers = new Headers(); req.headers.set("content-length", "5"); - const buf = new Buffer(enc.encode("Hello")); + const buf = new Buffer(encode("Hello")); req.r = new BufReader(buf); - const body = dec.decode(await Deno.readAll(req.body)); + const body = decode(await Deno.readAll(req.body)); assertEquals(body, "Hello"); } @@ -176,9 +132,9 @@ test(async function requestBodyWithContentLength(): Promise { const req = new ServerRequest(); req.headers = new Headers(); req.headers.set("Content-Length", "5000"); - const buf = new Buffer(enc.encode(longText)); + const buf = new Buffer(encode(longText)); req.r = new BufReader(buf); - const body = dec.decode(await Deno.readAll(req.body)); + const body = decode(await Deno.readAll(req.body)); assertEquals(body, longText); } // Handler ignored to consume body @@ -246,9 +202,9 @@ test(async function requestBodyWithTransferEncoding(): Promise { chunkOffset += chunkSize; } chunksData += "0\r\n\r\n"; - const buf = new Buffer(enc.encode(chunksData)); + const buf = new Buffer(encode(chunksData)); req.r = new BufReader(buf); - const body = dec.decode(await Deno.readAll(req.body)); + const body = decode(await Deno.readAll(req.body)); assertEquals(body, shortText); } @@ -270,9 +226,9 @@ test(async function requestBodyWithTransferEncoding(): Promise { chunkOffset += chunkSize; } chunksData += "0\r\n\r\n"; - const buf = new Buffer(enc.encode(chunksData)); + const buf = new Buffer(encode(chunksData)); req.r = new BufReader(buf); - const body = dec.decode(await Deno.readAll(req.body)); + const body = decode(await Deno.readAll(req.body)); assertEquals(body, longText); } }); @@ -283,14 +239,14 @@ test(async function requestBodyReaderWithContentLength(): Promise { const req = new ServerRequest(); req.headers = new Headers(); req.headers.set("content-length", "" + shortText.length); - const buf = new Buffer(enc.encode(shortText)); + const buf = new 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); assertNotEOF(nread); - const s = dec.decode(readBuf.subarray(0, nread as number)); + const s = decode(readBuf.subarray(0, nread as number)); assertEquals(shortText.substr(offset, nread as number), s); offset += nread as number; } @@ -304,14 +260,14 @@ test(async function requestBodyReaderWithContentLength(): Promise { const req = new ServerRequest(); req.headers = new Headers(); req.headers.set("Content-Length", "5000"); - const buf = new Buffer(enc.encode(longText)); + const buf = new 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); assertNotEOF(nread); - const s = dec.decode(readBuf.subarray(0, nread as number)); + const s = decode(readBuf.subarray(0, nread as number)); assertEquals(longText.substr(offset, nread as number), s); offset += nread as number; } @@ -338,14 +294,14 @@ test(async function requestBodyReaderWithTransferEncoding(): Promise { chunkOffset += chunkSize; } chunksData += "0\r\n\r\n"; - const buf = new Buffer(enc.encode(chunksData)); + const buf = new 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); assertNotEOF(nread); - const s = dec.decode(readBuf.subarray(0, nread as number)); + const s = decode(readBuf.subarray(0, nread as number)); assertEquals(shortText.substr(offset, nread as number), s); offset += nread as number; } @@ -371,14 +327,14 @@ test(async function requestBodyReaderWithTransferEncoding(): Promise { chunkOffset += chunkSize; } chunksData += "0\r\n\r\n"; - const buf = new Buffer(enc.encode(chunksData)); + const buf = new 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); assertNotEOF(nread); - const s = dec.decode(readBuf.subarray(0, nread as number)); + const s = decode(readBuf.subarray(0, nread as number)); assertEquals(longText.substr(offset, nread as number), s); offset += nread as number; } @@ -387,382 +343,99 @@ test(async function requestBodyReaderWithTransferEncoding(): Promise { } }); -test(async function writeUint8ArrayResponse(): Promise { - const shortText = "Hello"; - - const body = new TextEncoder().encode(shortText); - const res: Response = { body }; - - const buf = new Deno.Buffer(); - await writeResponse(buf, res); - - const decoder = new TextDecoder("utf-8"); - const reader = new BufReader(buf); - - let r: ReadLineResult; - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), `content-length: ${shortText.length}`); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(r.line.byteLength, 0); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), shortText); - assertEquals(r.more, false); - - const eof = await reader.readLine(); - assertEquals(eof, Deno.EOF); -}); - -test(async function writeStringResponse(): Promise { - const body = "Hello"; - - const res: Response = { body }; - - const buf = new Deno.Buffer(); - await writeResponse(buf, res); - - const decoder = new TextDecoder("utf-8"); - const reader = new BufReader(buf); - - let r: ReadLineResult; - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), `content-length: ${body.length}`); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(r.line.byteLength, 0); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), body); - assertEquals(r.more, false); - - const eof = await reader.readLine(); - assertEquals(eof, Deno.EOF); -}); - -test(async function writeStringReaderResponse(): Promise { - const shortText = "Hello"; - - const body = new StringReader(shortText); - const res: Response = { body }; - - const buf = new Deno.Buffer(); - await writeResponse(buf, res); - - const decoder = new TextDecoder("utf-8"); - const reader = new BufReader(buf); - - let r: ReadLineResult; - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), "transfer-encoding: chunked"); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(r.line.byteLength, 0); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), shortText.length.toString()); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), shortText); - assertEquals(r.more, false); - - r = assertNotEOF(await reader.readLine()); - assertEquals(decoder.decode(r.line), "0"); - assertEquals(r.more, false); -}); - -test("writeResponse with trailer", async () => { - const w = new Buffer(); - const body = new StringReader("Hello"); - await writeResponse(w, { - status: 200, - headers: new Headers({ - "transfer-encoding": "chunked", - trailer: "deno,node" - }), - body, - trailers: () => new Headers({ deno: "land", node: "js" }) +test("destroyed connection", async (): Promise => { + // Runs a simple server as another process + const p = Deno.run({ + args: [Deno.execPath(), "--allow-net", "http/testdata/simple_server.ts"], + stdout: "piped" }); - const ret = w.toString(); - const exp = [ - "HTTP/1.1 200 OK", - "transfer-encoding: chunked", - "trailer: deno,node", - "", - "5", - "Hello", - "0", - "", - "deno: land", - "node: js", - "", - "" - ].join("\r\n"); - assertEquals(ret, exp); -}); -test(async function readRequestError(): Promise { - const input = `GET / HTTP/1.1 -malformedHeader -`; - const reader = new BufReader(new StringReader(input)); - let err; try { - await readRequest(mockConn(), reader); - } catch (e) { - err = e; - } - assert(err instanceof Error); - assertEquals(err.message, "malformed MIME header line: malformedHeader"); -}); + const r = new TextProtoReader(new BufReader(p.stdout!)); + const s = await r.readLine(); + assert(s !== Deno.EOF && s.includes("server listening")); -// Ported from Go -// https://github.com/golang/go/blob/go1.12.5/src/net/http/request_test.go#L377-L443 -// TODO(zekth) fix tests -test(async function testReadRequestError(): Promise { - const testCases = [ - { - in: "GET / HTTP/1.1\r\nheader: foo\r\n\r\n", - headers: [{ key: "header", value: "foo" }] - }, - { - in: "GET / HTTP/1.1\r\nheader:foo\r\n", - err: UnexpectedEOFError - }, - { in: "", err: Deno.EOF }, - { - 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 | Deno.EOF | undefined; - try { - req = await readRequest(mockConn(), reader); - } catch (e) { - err = e; - } - if (test.err === Deno.EOF) { - assertEquals(req, Deno.EOF); - } else if (typeof test.err === "string") { - assertEquals(err.message, test.err); - } else if (test.err) { - assert(err instanceof (test.err as typeof UnexpectedEOFError)); - } else { - assert(req instanceof ServerRequest); - assert(test.headers); - assertEquals(err, undefined); - assertNotEquals(req, Deno.EOF); - for (const h of test.headers) { - assertEquals(req.headers.get(h.key), h.value); - } - } + let serverIsRunning = true; + p.status() + .then((): void => { + serverIsRunning = false; + }) + .catch((_): void => {}); // Ignores the error when closing the process. + + 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. + p.close(); } }); -// Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request_test.go#L535-L565 -test({ - name: "[http] parseHttpVersion", - fn(): 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 } - ]; - for (const t of testCases) { - let r, err; - try { - r = parseHTTPVersion(t.in); - } catch (e) { - err = e; - } - if (t.err) { - assert(err instanceof Error, t.in); - } else { - assertEquals(err, undefined); - assertEquals(r, t.want, t.in); - } - } - } -}); +test("serveTLS", async (): Promise => { + // Runs a simple server as another process + const p = Deno.run({ + args: [ + Deno.execPath(), + "--allow-net", + "--allow-read", + "http/testdata/simple_https_server.ts" + ], + stdout: "piped" + }); -test({ - name: "[http] destroyed connection", - async fn(): Promise { - // Runs a simple server as another process - const p = Deno.run({ - args: [Deno.execPath(), "--allow-net", "http/testdata/simple_server.ts"], - stdout: "piped" + try { + const r = new TextProtoReader(new BufReader(p.stdout!)); + const s = await r.readLine(); + assert( + s !== Deno.EOF && s.includes("server listening"), + "server must be started" + ); + + let serverIsRunning = true; + p.status() + .then((): void => { + serverIsRunning = false; + }) + .catch((_): void => {}); // Ignores the error when closing the process. + + // Requests to the server and immediately closes the connection + const conn = await Deno.connectTLS({ + hostname: "localhost", + port: 4503, + certFile: "http/testdata/tls/RootCA.pem" }); - - try { - const r = new TextProtoReader(new BufReader(p.stdout!)); - const s = await r.readLine(); - assert(s !== Deno.EOF && s.includes("server listening")); - - let serverIsRunning = true; - p.status() - .then((): void => { - serverIsRunning = false; - }) - .catch((_): void => {}); // Ignores the error when closing the process. - - 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. - p.close(); - } + await Deno.writeAll( + conn, + new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n") + ); + const res = new Uint8Array(100); + const nread = assertNotEOF(await conn.read(res)); + conn.close(); + const resStr = new TextDecoder().decode(res.subarray(0, nread)); + assert(resStr.includes("Hello HTTPS")); + assert(serverIsRunning); + } finally { + // Stops the sever. + p.close(); } }); -test({ - name: "[http] serveTLS", - async fn(): Promise { - // Runs a simple server as another process - const p = Deno.run({ - args: [ - Deno.execPath(), - "--allow-net", - "--allow-read", - "http/testdata/simple_https_server.ts" - ], - stdout: "piped" - }); +test("close server while iterating", async (): Promise => { + const server = serve(":8123"); + const nextWhileClosing = server[Symbol.asyncIterator]().next(); + server.close(); + assertEquals(await nextWhileClosing, { value: undefined, done: true }); - try { - const r = new TextProtoReader(new BufReader(p.stdout!)); - const s = await r.readLine(); - assert( - s !== Deno.EOF && s.includes("server listening"), - "server must be started" - ); - - let serverIsRunning = true; - p.status() - .then((): void => { - serverIsRunning = false; - }) - .catch((_): void => {}); // Ignores the error when closing the process. - - // Requests to the server and immediately closes the connection - const conn = await Deno.connectTLS({ - hostname: "localhost", - port: 4503, - certFile: "http/testdata/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 = assertNotEOF(await conn.read(res)); - conn.close(); - const resStr = new TextDecoder().decode(res.subarray(0, nread)); - assert(resStr.includes("Hello HTTPS")); - assert(serverIsRunning); - } finally { - // Stops the sever. - p.close(); - } - } -}); - -test({ - name: "[http] close server while iterating", - async fn(): Promise { - 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 }); - } + const nextAfterClosing = server[Symbol.asyncIterator]().next(); + assertEquals(await nextAfterClosing, { value: undefined, done: true }); }); // TODO(kevinkassimo): create a test that works on Windows. @@ -773,60 +446,57 @@ test({ // We need to find a way to similarly trigger an error on Windows so that // we can test if connection is closed. if (Deno.build.os !== "win") { - test({ - name: "[http] respond error handling", - async fn(): Promise { - const connClosedPromise = deferred(); - const serverRoutine = async (): Promise => { - let reqCount = 0; - const server = serve(":8124"); - // @ts-ignore - const serverRid = server.listener["rid"]; - let connRid = -1; - for await (const req of server) { - connRid = req.conn.rid; - reqCount++; - await Deno.readAll(req.body); - await connClosedPromise; - try { - await req.respond({ - body: new TextEncoder().encode("Hello World") - }); - await delay(100); - req.done = deferred(); - // This duplicate respond is to ensure we get a write failure from the - // other side. Our client would enter CLOSE_WAIT stage after close(), - // meaning first server .send (.respond) after close would still work. - // However, a second send would fail under RST, which is similar - // to the scenario where a failure happens during .respond - await req.respond({ - body: new TextEncoder().encode("Hello World") - }); - } catch { - break; - } + test("respond error handling", async (): Promise => { + const connClosedPromise = deferred(); + const serverRoutine = async (): Promise => { + let reqCount = 0; + const server = serve(":8124"); + // @ts-ignore + const serverRid = server.listener["rid"]; + let connRid = -1; + for await (const req of server) { + connRid = req.conn.rid; + reqCount++; + await Deno.readAll(req.body); + await connClosedPromise; + try { + await req.respond({ + body: new TextEncoder().encode("Hello World") + }); + await delay(100); + req.done = deferred(); + // This duplicate respond is to ensure we get a write failure from the + // other side. Our client would enter CLOSE_WAIT stage after close(), + // meaning first server .send (.respond) after close would still work. + // However, a second send would fail under RST, which is similar + // to the scenario where a failure happens during .respond + await req.respond({ + body: new TextEncoder().encode("Hello World") + }); + } catch { + break; } - server.close(); - const resources = Deno.resources(); - assert(reqCount === 1); - // Server should be gone - assert(!(serverRid in resources)); - // The connection should be destroyed - assert(!(connRid in resources)); - }; - 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(); // abruptly closing connection before response. - // conn on server side enters CLOSE_WAIT state. - connClosedPromise.resolve(); - await p; - } + } + server.close(); + const resources = Deno.resources(); + assert(reqCount === 1); + // Server should be gone + assert(!(serverRid in resources)); + // The connection should be destroyed + assert(!(connRid in resources)); + }; + 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(); // abruptly closing connection before response. + // conn on server side enters CLOSE_WAIT state. + connClosedPromise.resolve(); + await p; }); } diff --git a/std/ws/mod.ts b/std/ws/mod.ts index 3128a88b56..7a6e14a133 100644 --- a/std/ws/mod.ts +++ b/std/ws/mod.ts @@ -5,7 +5,7 @@ import { hasOwnProperty } from "../util/has_own_property.ts"; import { BufReader, BufWriter, UnexpectedEOFError } from "../io/bufio.ts"; import { readLong, readShort, sliceLongToBytes } from "../io/ioutil.ts"; import { Sha1 } from "./sha1.ts"; -import { writeResponse } from "../http/server.ts"; +import { writeResponse } from "../http/io.ts"; import { TextProtoReader } from "../textproto/mod.ts"; import { Deferred, deferred } from "../util/async.ts"; import { assertNotEOF } from "../testing/asserts.ts";