diff options
Diffstat (limited to 'cli/tests/unit/http_test.ts')
-rw-r--r-- | cli/tests/unit/http_test.ts | 2801 |
1 files changed, 0 insertions, 2801 deletions
diff --git a/cli/tests/unit/http_test.ts b/cli/tests/unit/http_test.ts deleted file mode 100644 index 66cc53113..000000000 --- a/cli/tests/unit/http_test.ts +++ /dev/null @@ -1,2801 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { Buffer, BufReader, BufWriter } from "@test_util/std/io/mod.ts"; -import { TextProtoReader } from "../testdata/run/textproto.ts"; -import { - assert, - assertEquals, - assertRejects, - assertStrictEquals, - assertThrows, - delay, - fail, -} from "./test_util.ts"; -import { join } from "@test_util/std/path/mod.ts"; - -const listenPort = 4507; -const listenPort2 = 4508; - -const { - buildCaseInsensitiveCommaValueFinder, - // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol -} = Deno[Deno.internal]; - -async function writeRequestAndReadResponse(conn: Deno.Conn): Promise<string> { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - - const w = new BufWriter(conn); - const r = new BufReader(conn); - const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`; - const writeResult = await w.write(encoder.encode(body)); - assertEquals(body.length, writeResult); - await w.flush(); - const tpr = new TextProtoReader(r); - const statusLine = await tpr.readLine(); - assert(statusLine !== null); - const headers = await tpr.readMimeHeader(); - assert(headers !== null); - - const chunkedReader = chunkedBodyReader(headers, r); - const buf = new Uint8Array(5); - const dest = new Buffer(); - let result: number | null; - while ((result = await chunkedReader.read(buf)) !== null) { - const len = Math.min(buf.byteLength, result); - await dest.write(buf.subarray(0, len)); - } - return decoder.decode(dest.bytes()); -} - -Deno.test({ permissions: { net: true } }, async function httpServerBasic() { - let httpConn: Deno.HttpConn; - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const reqEvent = await httpConn.nextRequest(); - assert(reqEvent); - const { request, respondWith } = reqEvent; - assertEquals(new URL(request.url).href, `http://127.0.0.1:${listenPort}/`); - assertEquals(await request.text(), ""); - await respondWith( - new Response("Hello World", { headers: { "foo": "bar" } }), - ); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { - headers: { "connection": "close" }, - }); - const clone = resp.clone(); - const text = await resp.text(); - assertEquals(text, "Hello World"); - assertEquals(resp.headers.get("foo"), "bar"); - const cloneText = await clone.text(); - assertEquals(cloneText, "Hello World"); - await promise; - - httpConn!.close(); -}); - -// https://github.com/denoland/deno/issues/15107 -Deno.test( - { permissions: { net: true } }, - async function httpLazyHeadersIssue15107() { - let headers: Headers; - const promise = (async () => { - const listener = Deno.listen({ port: 2333 }); - const conn = await listener.accept(); - listener.close(); - const httpConn = Deno.serveHttp(conn); - const e = await httpConn.nextRequest(); - assert(e); - const { request } = e; - request.text(); - headers = request.headers; - httpConn!.close(); - })(); - - const conn = await Deno.connect({ port: 2333 }); - // Send GET request with a body + content-length. - const encoder = new TextEncoder(); - const body = - `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`; - const writeResult = await conn.write(encoder.encode(body)); - assertEquals(body.length, writeResult); - await promise; - conn.close(); - assertEquals(headers!.get("content-length"), "5"); - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpReadHeadersAfterClose() { - const promise = (async () => { - const listener = Deno.listen({ port: 2334 }); - const conn = await listener.accept(); - listener.close(); - const httpConn = Deno.serveHttp(conn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - - await request.text(); // Read body - await respondWith(new Response("Hello World")); // Closes request - - assertThrows(() => request.headers, TypeError, "request closed"); - httpConn!.close(); - })(); - - const conn = await Deno.connect({ port: 2334 }); - // Send GET request with a body + content-length. - const encoder = new TextEncoder(); - const body = - `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`; - const writeResult = await conn.write(encoder.encode(body)); - assertEquals(body.length, writeResult); - await promise; - conn.close(); - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerGetRequestBody() { - let httpConn: Deno.HttpConn; - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.body, null); - await respondWith(new Response("", { headers: {} })); - })(); - - const conn = await Deno.connect({ port: listenPort }); - // Send GET request with a body + content-length. - const encoder = new TextEncoder(); - const body = - `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nContent-Length: 5\r\n\r\n12345`; - const writeResult = await conn.write(encoder.encode(body)); - assertEquals(body.length, writeResult); - - const resp = new Uint8Array(200); - const readResult = await conn.read(resp); - assertEquals(readResult, 138); - - conn.close(); - - await promise; - httpConn!.close(); - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerStreamResponse() { - const stream = new TransformStream(); - const writer = stream.writable.getWriter(); - writer.write(new TextEncoder().encode("hello ")); - writer.write(new TextEncoder().encode("world")); - writer.close(); - - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ port: listenPort }); - const promise = (async () => { - const conn = await listener.accept(); - httpConn = Deno.serveHttp(conn); - const evt = await httpConn.nextRequest(); - assert(evt); - const { request, respondWith } = evt; - assert(!request.body); - await respondWith(new Response(stream.readable)); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`); - const respBody = await resp.text(); - assertEquals("hello world", respBody); - await promise; - httpConn!.close(); - listener.close(); - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerStreamRequest() { - const stream = new TransformStream(); - const writer = stream.writable.getWriter(); - writer.write(new TextEncoder().encode("hello ")); - writer.write(new TextEncoder().encode("world")); - writer.close(); - - const listener = Deno.listen({ port: listenPort }); - const promise = (async () => { - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const evt = await httpConn.nextRequest(); - assert(evt); - const { request, respondWith } = evt; - const reqBody = await request.text(); - assertEquals("hello world", reqBody); - await respondWith(new Response("")); - - // TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending - // request for url (https://localhost:${listenPort}/): connection closed before - // message completed". - assertEquals(await httpConn.nextRequest(), null); - - listener.close(); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { - body: stream.readable, - method: "POST", - headers: { "connection": "close" }, - }); - - await resp.arrayBuffer(); - await promise; - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerStreamDuplex() { - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ port: listenPort }); - const promise = (async () => { - const conn = await listener.accept(); - httpConn = Deno.serveHttp(conn); - const evt = await httpConn.nextRequest(); - assert(evt); - const { request, respondWith } = evt; - assert(request.body); - await respondWith(new Response(request.body)); - })(); - - const ts = new TransformStream(); - const writable = ts.writable.getWriter(); - const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { - method: "POST", - body: ts.readable, - }); - assert(resp.body); - const reader = resp.body.getReader(); - await writable.write(new Uint8Array([1])); - const chunk1 = await reader.read(); - assert(!chunk1.done); - assertEquals(chunk1.value, new Uint8Array([1])); - await writable.write(new Uint8Array([2])); - const chunk2 = await reader.read(); - assert(!chunk2.done); - assertEquals(chunk2.value, new Uint8Array([2])); - - await writable.close(); - const chunk3 = await reader.read(); - assert(chunk3.done); - await promise; - httpConn!.close(); - listener.close(); - }, -); - -Deno.test({ permissions: { net: true } }, async function httpServerClose() { - const listener = Deno.listen({ port: listenPort }); - const client = await Deno.connect({ port: listenPort }); - const httpConn = Deno.serveHttp(await listener.accept()); - client.close(); - const evt = await httpConn.nextRequest(); - assertEquals(evt, null); - // Note httpConn is automatically closed when "done" is reached. - listener.close(); -}); - -Deno.test( - { permissions: { net: true } }, - async function httpServerInvalidMethod() { - const listener = Deno.listen({ port: listenPort }); - const client = await Deno.connect({ port: listenPort }); - const httpConn = Deno.serveHttp(await listener.accept()); - await client.write(new Uint8Array([1, 2, 3])); - await assertRejects( - async () => { - await httpConn.nextRequest(); - }, - Deno.errors.Http, - "invalid HTTP method parsed", - ); - // Note httpConn is automatically closed when it errors. - client.close(); - listener.close(); - }, -); - -Deno.test( - { permissions: { read: true, net: true } }, - async function httpServerWithTls() { - const hostname = "localhost"; - const port = listenPort; - - const promise = (async () => { - const listener = Deno.listenTls({ - hostname, - port, - cert: Deno.readTextFileSync("cli/tests/testdata/tls/localhost.crt"), - key: Deno.readTextFileSync("cli/tests/testdata/tls/localhost.key"), - }); - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const evt = await httpConn.nextRequest(); - assert(evt); - const { respondWith } = evt; - await respondWith(new Response("Hello World")); - - // TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending - // request for url (https://localhost:${listenPort}/): connection closed before - // message completed". - assertEquals(await httpConn.nextRequest(), null); - - listener.close(); - })(); - - const caCert = Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem"); - const client = Deno.createHttpClient({ caCerts: [caCert] }); - const resp = await fetch(`https://${hostname}:${port}/`, { - headers: { "connection": "close" }, - client, - }); - client.close(); - const respBody = await resp.text(); - assertEquals("Hello World", respBody); - await promise; - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerRegressionHang() { - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ port: listenPort }); - const promise = (async () => { - const conn = await listener.accept(); - httpConn = Deno.serveHttp(conn); - const event = await httpConn.nextRequest(); - assert(event); - const { request, respondWith } = event; - const reqBody = await request.text(); - assertEquals("request", reqBody); - await respondWith(new Response("response")); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { - method: "POST", - body: "request", - }); - const respBody = await resp.text(); - assertEquals("response", respBody); - await promise; - - httpConn!.close(); - listener.close(); - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerCancelBodyOnResponseFailure() { - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const event = await httpConn.nextRequest(); - assert(event); - const { respondWith } = event; - let cancelReason: string; - await assertRejects( - async () => { - let interval = 0; - await respondWith( - new Response( - new ReadableStream({ - start(controller) { - interval = setInterval(() => { - const message = `data: ${Date.now()}\n\n`; - controller.enqueue(new TextEncoder().encode(message)); - }, 200); - }, - cancel(reason) { - cancelReason = reason; - clearInterval(interval); - }, - }), - ), - ); - }, - Deno.errors.Http, - cancelReason!, - ); - assert(cancelReason!); - httpConn!.close(); - listener.close(); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`); - await resp.body!.cancel(); - await promise; - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerNextRequestErrorExposedInResponse() { - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const event = await httpConn.nextRequest(); - assert(event); - // Start polling for the next request before awaiting response. - const nextRequestPromise = httpConn.nextRequest(); - const { respondWith } = event; - await assertRejects( - async () => { - let interval = 0; - await respondWith( - new Response( - new ReadableStream({ - start(controller) { - interval = setInterval(() => { - const message = `data: ${Date.now()}\n\n`; - controller.enqueue(new TextEncoder().encode(message)); - }, 200); - }, - cancel() { - clearInterval(interval); - }, - }), - ), - ); - }, - Deno.errors.Http, - "connection closed", - ); - // The error from `op_http_accept` reroutes to `respondWith()`. - assertEquals(await nextRequestPromise, null); - listener.close(); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`); - await resp.body!.cancel(); - await promise; - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerEmptyBlobResponse() { - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ port: listenPort }); - const promise = (async () => { - const conn = await listener.accept(); - httpConn = Deno.serveHttp(conn); - const event = await httpConn.nextRequest(); - assert(event); - const { respondWith } = event; - await respondWith(new Response(new Blob([]))); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`); - const respBody = await resp.text(); - assertEquals("", respBody); - await promise; - httpConn!.close(); - listener.close(); - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerNextRequestResolvesOnClose() { - const httpConnList: Deno.HttpConn[] = []; - - async function serve(l: Deno.Listener) { - for await (const conn of l) { - (async () => { - const c = Deno.serveHttp(conn); - httpConnList.push(c); - for await (const { respondWith } of c) { - respondWith(new Response("hello")); - } - })(); - } - } - - const l = Deno.listen({ port: listenPort }); - serve(l); - - await delay(300); - const res = await fetch(`http://localhost:${listenPort}/`); - const _text = await res.text(); - - // Close connection and listener. - httpConnList.forEach((conn) => conn.close()); - l.close(); - - await delay(300); - }, -); - -Deno.test( - { permissions: { net: true } }, - // Issue: https://github.com/denoland/deno/issues/10870 - async function httpServerHang() { - // Quick and dirty way to make a readable stream from a string. Alternatively, - // `readableStreamFromReader(file)` could be used. - function stream(s: string): ReadableStream<Uint8Array> { - return new Response(s).body!; - } - - const httpConns: Deno.HttpConn[] = []; - const promise = (async () => { - let count = 0; - const listener = Deno.listen({ port: listenPort }); - for await (const conn of listener) { - (async () => { - const httpConn = Deno.serveHttp(conn); - httpConns.push(httpConn); - for await (const { respondWith } of httpConn) { - respondWith(new Response(stream("hello"))); - - count++; - if (count >= 2) { - listener.close(); - } - } - })(); - } - })(); - - const clientConn = await Deno.connect({ port: listenPort }); - - const r1 = await writeRequestAndReadResponse(clientConn); - assertEquals(r1, "hello"); - - const r2 = await writeRequestAndReadResponse(clientConn); - assertEquals(r2, "hello"); - - clientConn.close(); - await promise; - for (const conn of httpConns) { - conn.close(); - } - }, -); - -Deno.test( - { permissions: { net: true } }, - // Issue: https://github.com/denoland/deno/issues/10930 - async function httpServerStreamingResponse() { - // This test enqueues a single chunk for readable - // stream and waits for client to read that chunk and signal - // it before enqueueing subsequent chunk. Issue linked above - // presented a situation where enqueued chunks were not - // written to the HTTP connection until the next chunk was enqueued. - - let counter = 0; - - const deferreds = [ - Promise.withResolvers<void>(), - Promise.withResolvers<void>(), - Promise.withResolvers<void>(), - ]; - - async function writeRequest(conn: Deno.Conn) { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - - const w = new BufWriter(conn); - const r = new BufReader(conn); - const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`; - const writeResult = await w.write(encoder.encode(body)); - assertEquals(body.length, writeResult); - await w.flush(); - const tpr = new TextProtoReader(r); - const statusLine = await tpr.readLine(); - assert(statusLine !== null); - const headers = await tpr.readMimeHeader(); - assert(headers !== null); - - const chunkedReader = chunkedBodyReader(headers, r); - const buf = new Uint8Array(5); - const dest = new Buffer(); - let result: number | null; - while ((result = await chunkedReader.read(buf)) !== null) { - const len = Math.min(buf.byteLength, result); - await dest.write(buf.subarray(0, len)); - // Resolve a deferred - this will make response stream to - // enqueue next chunk. - deferreds[counter - 1].resolve(); - } - return decoder.decode(dest.bytes()); - } - - function periodicStream() { - return new ReadableStream({ - start(controller) { - controller.enqueue(`${counter}\n`); - counter++; - }, - - async pull(controller) { - if (counter >= 3) { - return controller.close(); - } - - await deferreds[counter - 1].promise; - - controller.enqueue(`${counter}\n`); - counter++; - }, - }).pipeThrough(new TextEncoderStream()); - } - - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ port: listenPort }); - const finished = (async () => { - const conn = await listener.accept(); - httpConn = Deno.serveHttp(conn); - const requestEvent = await httpConn.nextRequest(); - const { respondWith } = requestEvent!; - await respondWith(new Response(periodicStream())); - })(); - - // start a client - const clientConn = await Deno.connect({ port: listenPort }); - - const r1 = await writeRequest(clientConn); - assertEquals(r1, "0\n1\n2\n"); - - await finished; - clientConn.close(); - - httpConn!.close(); - listener.close(); - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpRequestLatin1Headers() { - let httpConn: Deno.HttpConn; - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const reqEvent = await httpConn.nextRequest(); - assert(reqEvent); - const { request, respondWith } = reqEvent; - assertEquals(request.headers.get("X-Header-Test"), "á"); - await respondWith( - new Response("", { headers: { "X-Header-Test": "Æ" } }), - ); - })(); - - const clientConn = await Deno.connect({ port: listenPort }); - const requestText = - `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nX-Header-Test: á\r\n\r\n`; - const requestBytes = new Uint8Array(requestText.length); - for (let i = 0; i < requestText.length; i++) { - requestBytes[i] = requestText.charCodeAt(i); - } - let written = 0; - while (written < requestBytes.byteLength) { - written += await clientConn.write(requestBytes.slice(written)); - } - - let responseText = ""; - const buf = new Uint8Array(1024); - let read; - - while ((read = await clientConn.read(buf)) !== null) { - httpConn!.close(); - for (let i = 0; i < read; i++) { - responseText += String.fromCharCode(buf[i]); - } - } - - clientConn.close(); - - assert(/\r\n[Xx]-[Hh]eader-[Tt]est: Æ\r\n/.test(responseText)); - - await promise; - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerRequestWithoutPath() { - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ port: listenPort }); - const promise = (async () => { - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const reqEvent = await httpConn.nextRequest(); - assert(reqEvent); - const { request, respondWith } = reqEvent; - assertEquals( - new URL(request.url).href, - `http://127.0.0.1:${listenPort}/`, - ); - assertEquals(await request.text(), ""); - await respondWith(new Response()); - })(); - - const clientConn = await Deno.connect({ port: listenPort }); - - async function writeRequest(conn: Deno.Conn) { - const encoder = new TextEncoder(); - - const w = new BufWriter(conn); - const r = new BufReader(conn); - const body = - `CONNECT 127.0.0.1:${listenPort} HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`; - const writeResult = await w.write(encoder.encode(body)); - assertEquals(body.length, writeResult); - await w.flush(); - const tpr = new TextProtoReader(r); - const statusLine = await tpr.readLine(); - assert(statusLine !== null); - const m = statusLine.match(/^(.+?) (.+?) (.+?)$/); - assert(m !== null, "must be matched"); - const [_, _proto, status, _ok] = m; - assertEquals(status, "200"); - const headers = await tpr.readMimeHeader(); - assert(headers !== null); - } - - await writeRequest(clientConn); - clientConn.close(); - await promise; - httpConn!.close(); - }, -); - -Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() { - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - listener.close(); - const httpConn = Deno.serveHttp(conn); - const reqEvent = await httpConn.nextRequest(); - assert(reqEvent); - const { request, respondWith } = reqEvent; - const { - response, - socket, - } = Deno.upgradeWebSocket(request); - socket.onerror = () => fail(); - socket.onmessage = (m) => { - socket.send(m.data); - socket.close(1001); - }; - const close = new Promise<void>((resolve) => { - socket.onclose = () => resolve(); - }); - await respondWith(response); - await close; - })(); - - const def = Promise.withResolvers<void>(); - const ws = new WebSocket(`ws://localhost:${listenPort}`); - ws.onmessage = (m) => assertEquals(m.data, "foo"); - ws.onerror = () => fail(); - ws.onclose = () => def.resolve(); - ws.onopen = () => ws.send("foo"); - await def.promise; - await promise; -}); - -Deno.test(function httpUpgradeWebSocket() { - const request = new Request("https://deno.land/", { - headers: { - connection: "Upgrade", - upgrade: "websocket", - "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", - }, - }); - const { response } = Deno.upgradeWebSocket(request); - assertEquals(response.status, 101); - assertEquals(response.headers.get("connection"), "Upgrade"); - assertEquals(response.headers.get("upgrade"), "websocket"); - assertEquals( - response.headers.get("sec-websocket-accept"), - "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", - ); -}); - -Deno.test(function httpUpgradeWebSocketMultipleConnectionOptions() { - const request = new Request("https://deno.land/", { - headers: { - connection: "keep-alive, upgrade", - upgrade: "websocket", - "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", - }, - }); - const { response } = Deno.upgradeWebSocket(request); - assertEquals(response.status, 101); -}); - -Deno.test(function httpUpgradeWebSocketMultipleUpgradeOptions() { - const request = new Request("https://deno.land/", { - headers: { - connection: "upgrade", - upgrade: "websocket, foo", - "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", - }, - }); - const { response } = Deno.upgradeWebSocket(request); - assertEquals(response.status, 101); -}); - -Deno.test(function httpUpgradeWebSocketCaseInsensitiveUpgradeHeader() { - const request = new Request("https://deno.land/", { - headers: { - connection: "upgrade", - upgrade: "Websocket", - "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", - }, - }); - const { response } = Deno.upgradeWebSocket(request); - assertEquals(response.status, 101); -}); - -Deno.test(function httpUpgradeWebSocketInvalidUpgradeHeader() { - assertThrows( - () => { - const request = new Request("https://deno.land/", { - headers: { - connection: "upgrade", - upgrade: "invalid", - "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", - }, - }); - Deno.upgradeWebSocket(request); - }, - TypeError, - "Invalid Header: 'upgrade' header must contain 'websocket'", - ); -}); - -Deno.test(function httpUpgradeWebSocketWithoutUpgradeHeader() { - assertThrows( - () => { - const request = new Request("https://deno.land/", { - headers: { - connection: "upgrade", - "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", - }, - }); - Deno.upgradeWebSocket(request); - }, - TypeError, - "Invalid Header: 'upgrade' header must contain 'websocket'", - ); -}); - -Deno.test( - { permissions: { net: true } }, - async function httpCookieConcatenation() { - let httpConn: Deno.HttpConn; - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const reqEvent = await httpConn.nextRequest(); - assert(reqEvent); - const { request, respondWith } = reqEvent; - assertEquals( - new URL(request.url).href, - `http://127.0.0.1:${listenPort}/`, - ); - assertEquals(await request.text(), ""); - assertEquals(request.headers.get("cookie"), "foo=bar; bar=foo"); - await respondWith(new Response("ok")); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { - headers: [ - ["connection", "close"], - ["cookie", "foo=bar"], - ["cookie", "bar=foo"], - ], - }); - const text = await resp.text(); - assertEquals(text, "ok"); - await promise; - httpConn!.close(); - }, -); - -// https://github.com/denoland/deno/issues/11651 -Deno.test({ permissions: { net: true } }, async function httpServerPanic() { - const listener = Deno.listen({ port: listenPort }); - const client = await Deno.connect({ port: listenPort }); - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - - // This message is incomplete on purpose, we'll forcefully close client connection - // after it's flushed to cause connection to error out on the server side. - const encoder = new TextEncoder(); - await client.write(encoder.encode("GET / HTTP/1.1")); - - httpConn.nextRequest(); - await client.write(encoder.encode("\r\n\r\n")); - httpConn!.close(); - - client.close(); - listener.close(); -}); - -Deno.test( - { permissions: { net: true, write: true, read: true } }, - async function httpServerCorrectSizeResponse() { - const tmpFile = await Deno.makeTempFile(); - using file = await Deno.open(tmpFile, { write: true, read: true }); - await file.write(new Uint8Array(70 * 1024).fill(1)); // 70kb sent in 64kb + 6kb chunks - - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ port: listenPort }); - const promise = (async () => { - const conn = await listener.accept(); - httpConn = Deno.serveHttp(conn); - const ev = await httpConn.nextRequest(); - const { respondWith } = ev!; - const f = await Deno.open(tmpFile, { read: true }); - await respondWith(new Response(f.readable, { status: 200 })); - })(); - const resp = await fetch(`http://127.0.0.1:${listenPort}/`); - const body = await resp.arrayBuffer(); - assertEquals(body.byteLength, 70 * 1024); - await promise; - httpConn!.close(); - listener.close(); - }, -); - -Deno.test( - { permissions: { net: true, write: true, read: true } }, - async function httpServerClosedStream() { - const listener = Deno.listen({ port: listenPort }); - - const client = await Deno.connect({ port: listenPort }); - await client.write(new TextEncoder().encode( - `GET / HTTP/1.0\r\n\r\n`, - )); - - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const ev = await httpConn.nextRequest(); - const { respondWith } = ev!; - - const tmpFile = await Deno.makeTempFile(); - const file = await Deno.open(tmpFile, { write: true, read: true }); - await file.write(new TextEncoder().encode("hello")); - - const reader = await file.readable.getReader(); - while (true) { - const { done, value } = await reader.read(); - if (done) break; - assert(value); - } - - let didThrow = false; - try { - await respondWith(new Response(file.readable)); - } catch { - // pass - didThrow = true; - } - - assert(didThrow); - httpConn!.close(); - listener.close(); - client.close(); - }, -); - -// https://github.com/denoland/deno/issues/11595 -Deno.test( - { permissions: { net: true } }, - async function httpServerIncompleteMessage() { - const listener = Deno.listen({ port: listenPort }); - - const client = await Deno.connect({ port: listenPort }); - await client.write(new TextEncoder().encode( - `GET / HTTP/1.0\r\n\r\n`, - )); - - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const ev = await httpConn.nextRequest(); - const { respondWith } = ev!; - - const errors: Error[] = []; - - const readable = new ReadableStream({ - async pull(controller) { - client.close(); - await delay(1000); - controller.enqueue(new TextEncoder().encode( - "written to the writable side of a TransformStream", - )); - controller.close(); - }, - cancel(error) { - errors.push(error); - }, - }); - - const res = new Response(readable); - - await respondWith(res).catch((error: Error) => errors.push(error)); - - httpConn!.close(); - listener.close(); - - assert(errors.length >= 1); - for (const error of errors) { - assertEquals(error.name, "Http"); - assert(error.message.includes("connection")); - } - }, -); - -// https://github.com/denoland/deno/issues/11743 -Deno.test( - { permissions: { net: true } }, - async function httpServerDoesntLeakResources() { - const listener = Deno.listen({ port: listenPort }); - const [conn, clientConn] = await Promise.all([ - listener.accept(), - Deno.connect({ port: listenPort }), - ]); - const httpConn = Deno.serveHttp(conn); - - await Promise.all([ - httpConn.nextRequest(), - clientConn.write(new TextEncoder().encode( - `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`, - )), - ]); - - httpConn!.close(); - listener.close(); - clientConn.close(); - }, -); - -// https://github.com/denoland/deno/issues/11926 -// verify that the only new resource is "httpConnection", to make -// sure "request" resource is closed even if its body was not read -// by server handler -Deno.test( - { permissions: { net: true } }, - async function httpServerDoesntLeakResources2() { - let listener: Deno.Listener; - let httpConn: Deno.HttpConn; - - const promise = (async () => { - listener = Deno.listen({ port: listenPort }); - for await (const conn of listener) { - httpConn = Deno.serveHttp(conn); - for await (const { request, respondWith } of httpConn) { - assertEquals( - new URL(request.url).href, - `http://127.0.0.1:${listenPort}/`, - ); - // not reading request body on purpose - respondWith(new Response("ok")); - } - } - })(); - - const response = await fetch(`http://127.0.0.1:${listenPort}`, { - method: "POST", - body: "hello world", - }); - await response.text(); - - listener!.close(); - httpConn!.close(); - await promise; - }, -); - -// https://github.com/denoland/deno/pull/12216 -Deno.test( - { permissions: { net: true } }, - async function droppedConnSenderNoPanic() { - async function server() { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - const http = Deno.serveHttp(conn); - const evt = await http.nextRequest(); - http.close(); - try { - await evt!.respondWith(new Response("boom")); - } catch { - // Ignore error. - } - listener.close(); - } - - async function client() { - try { - const resp = await fetch(`http://127.0.0.1:${listenPort}/`); - await resp.body?.cancel(); - } catch { - // Ignore error - } - } - - await Promise.all([server(), client()]); - }, -); - -// https://github.com/denoland/deno/issues/12193 -Deno.test( - { permissions: { net: true } }, - async function httpConnConcurrentNextRequestCalls() { - const hostname = "localhost"; - const port = listenPort; - - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ hostname, port }); - async function server() { - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const promises = new Array(10).fill(null).map(async (_, i) => { - const event = await httpConn.nextRequest(); - assert(event); - const { pathname } = new URL(event.request.url); - assertStrictEquals(pathname, `/${i}`); - const response = new Response(`Response #${i}`); - await event.respondWith(response); - }); - await Promise.all(promises); - } - - async function client() { - for (let i = 0; i < 10; i++) { - const response = await fetch(`http://${hostname}:${port}/${i}`); - const body = await response.text(); - assertStrictEquals(body, `Response #${i}`); - } - } - - await Promise.all([server(), delay(100).then(client)]); - httpConn!.close(); - listener.close(); - }, -); - -// https://github.com/denoland/deno/pull/12704 -// https://github.com/denoland/deno/pull/12732 -Deno.test( - { permissions: { net: true } }, - async function httpConnAutoCloseDelayedOnUpgrade() { - const hostname = "localhost"; - const port = listenPort; - - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - const httpConn = Deno.serveHttp(tcpConn); - - const event1 = await httpConn.nextRequest() as Deno.RequestEvent; - const event2Promise = httpConn.nextRequest(); - - const { socket, response } = Deno.upgradeWebSocket(event1.request); - socket.onmessage = (event) => socket.send(event.data); - const socketClosed = new Promise<void>((resolve) => { - socket.onclose = () => resolve(); - }); - event1.respondWith(response); - - const event2 = await event2Promise; - assertStrictEquals(event2, null); - - listener.close(); - await socketClosed; - } - - async function client() { - const socket = new WebSocket(`ws://${hostname}:${port}/`); - socket.onopen = () => socket.send("bla bla"); - const closed = new Promise<void>((resolve) => { - socket.onclose = () => resolve(); - }); - const { data } = await new Promise<MessageEvent<string>>((res) => - socket.onmessage = res - ); - assertStrictEquals(data, "bla bla"); - socket.close(); - await closed; - } - - await Promise.all([server(), client()]); - }, -); - -// https://github.com/denoland/deno/issues/12741 -// https://github.com/denoland/deno/pull/12746 -// https://github.com/denoland/deno/pull/12798 -Deno.test( - { permissions: { net: true, run: true } }, - async function httpServerDeleteRequestHasBody() { - const hostname = "localhost"; - const port = listenPort; - - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ hostname, port }); - async function server() { - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const event = await httpConn.nextRequest() as Deno.RequestEvent; - assert(event.request.body); - const response = new Response(); - await event.respondWith(response); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = ["-X", "DELETE", url]; - const { success } = await new Deno.Command("curl", { - args, - stdout: "null", - stderr: "null", - }).output(); - assert(success); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - listener.close(); - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerRespondNonAsciiUint8Array() { - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ port: listenPort }); - const promise = (async () => { - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.body, null); - await respondWith( - new Response(new Uint8Array([128]), {}), - ); - })(); - - const resp = await fetch(`http://localhost:${listenPort}/`); - assertEquals(resp.status, 200); - const body = await resp.arrayBuffer(); - assertEquals(new Uint8Array(body), new Uint8Array([128])); - - await promise; - httpConn!.close(); - }, -); - -function tmpUnixSocketPath(): string { - const folder = Deno.makeTempDirSync(); - return join(folder, "socket"); -} - -// https://github.com/denoland/deno/pull/13628 -Deno.test( - { - ignore: Deno.build.os === "windows", - permissions: { read: true, write: true }, - }, - async function httpServerOnUnixSocket() { - const filePath = tmpUnixSocketPath(); - - let httpConn: Deno.HttpConn; - const promise = (async () => { - const listener = Deno.listen({ path: filePath, transport: "unix" }); - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const reqEvent = await httpConn.nextRequest(); - assert(reqEvent); - const { request, respondWith } = reqEvent; - const url = new URL(request.url); - assertEquals(url.protocol, "http+unix:"); - assertEquals(decodeURIComponent(url.host), filePath); - assertEquals(url.pathname, "/path/name"); - await respondWith(new Response("", { headers: {} })); - })(); - - // fetch() does not supports unix domain sockets yet https://github.com/denoland/deno/issues/8821 - const conn = await Deno.connect({ path: filePath, transport: "unix" }); - const encoder = new TextEncoder(); - // The Host header must be present and empty if it is not a Internet host name (RFC2616, Section 14.23) - const body = `GET /path/name HTTP/1.1\r\nHost:\r\n\r\n`; - const writeResult = await conn.write(encoder.encode(body)); - assertEquals(body.length, writeResult); - - const resp = new Uint8Array(200); - const readResult = await conn.read(resp); - assertEquals(readResult, 138); - - conn.close(); - - await promise; - httpConn!.close(); - }, -); - -/* Automatic Body Compression */ - -const decoder = new TextDecoder(); - -Deno.test({ - name: "http server compresses body - check headers", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - const listener = Deno.listen({ hostname, port }); - - const data = { hello: "deno", now: "with", compressed: "body" }; - - let httpConn: Deno.HttpConn; - async function server() { - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const response = new Response(JSON.stringify(data), { - headers: { "content-type": "application/json" }, - }); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert(output.includes("content-encoding: gzip\r\n")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server compresses body - check body", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - const listener = Deno.listen({ hostname, port }); - - const data = { hello: "deno", now: "with", compressed: "body" }; - - let httpConn: Deno.HttpConn; - async function server() { - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const response = new Response(JSON.stringify(data), { - headers: { "content-type": "application/json" }, - }); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const proc = new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).spawn(); - const status = await proc.status; - assert(status.success); - const stdout = proc.stdout - .pipeThrough(new DecompressionStream("gzip")) - .pipeThrough(new TextDecoderStream()); - let body = ""; - for await (const chunk of stdout) { - body += chunk; - } - assertEquals(JSON.parse(body), data); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server doesn't compress small body", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const response = new Response( - JSON.stringify({ hello: "deno" }), - { - headers: { "content-type": "application/json" }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout).toLocaleLowerCase(); - assert(output.includes("vary: accept-encoding\r\n")); - assert(!output.includes("content-encoding: ")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server respects accept-encoding weights", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals( - request.headers.get("Accept-Encoding"), - "gzip;q=0.8, br;q=1.0, *;q=0.1", - ); - const response = new Response( - JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), - { - headers: { "content-type": "application/json" }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip;q=0.8, br;q=1.0, *;q=0.1", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert(output.includes("content-encoding: br\r\n")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server augments vary header", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const response = new Response( - JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), - { - headers: { "content-type": "application/json", vary: "Accept" }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout); - assert(output.includes("vary: Accept-Encoding, Accept\r\n")); - assert(output.includes("content-encoding: gzip\r\n")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server weakens etag header", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const response = new Response( - JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), - { - headers: { - "content-type": "application/json", - etag: "33a64df551425fcc55e4d42a148795d9f25f89d4", - }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "curl", - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert( - output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"), - ); - assert(output.includes("content-encoding: gzip\r\n")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server passes through weak etag header", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const response = new Response( - JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), - { - headers: { - "content-type": "application/json", - etag: "W/33a64df551425fcc55e4d42a148795d9f25f89d4", - }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert( - output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"), - ); - assert(output.includes("content-encoding: gzip\r\n")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server doesn't compress body when no-transform is set", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const response = new Response( - JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), - { - headers: { - "content-type": "application/json", - "cache-control": "no-transform", - }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert(!output.includes("content-encoding: ")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server doesn't compress body when content-range is set", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const response = new Response( - JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), - { - headers: { - "content-type": "application/json", - "content-range": "bytes 200-100/67589", - }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert(!output.includes("content-encoding: ")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server compresses streamed bodies - check headers", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - const encoder = new TextEncoder(); - const listener = Deno.listen({ hostname, port }); - - const data = { hello: "deno", now: "with", compressed: "body" }; - - let httpConn: Deno.HttpConn; - async function server() { - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const bodyInit = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(JSON.stringify(data))); - controller.close(); - }, - }); - const response = new Response( - bodyInit, - { headers: { "content-type": "application/json" } }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "curl", - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert(output.includes("content-encoding: gzip\r\n")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server compresses streamed bodies - check body", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - const encoder = new TextEncoder(); - const listener = Deno.listen({ hostname, port }); - - const data = { hello: "deno", now: "with", compressed: "body" }; - - let httpConn: Deno.HttpConn; - async function server() { - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const bodyInit = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(JSON.stringify(data))); - controller.close(); - }, - }); - const response = new Response( - bodyInit, - { headers: { "content-type": "application/json" } }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const proc = new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).spawn(); - const status = await proc.status; - assert(status.success); - const stdout = proc.stdout - .pipeThrough(new DecompressionStream("gzip")) - .pipeThrough(new TextDecoderStream()); - let body = ""; - for await (const chunk of stdout) { - body += chunk; - } - assertEquals(JSON.parse(body), data); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server updates content-length header if compression is applied", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - let contentLength: string; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const body = JSON.stringify({ - hello: "deno", - now: "with", - compressed: "body", - }); - contentLength = String(body.length); - const response = new Response( - body, - { - headers: { - "content-type": "application/json", - "content-length": contentLength, - }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "-i", - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - ]; - const { success, stdout } = await new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).output(); - assert(success); - const output = decoder.decode(stdout); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert(output.includes("content-encoding: gzip\r\n")); - // Ensure the content-length header is updated (but don't check the exact length). - assert(!output.includes(`content-length: ${contentLength}\r\n`)); - assert(output.includes("content-length: ")); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server compresses when accept-encoding is deflate, gzip", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - let contentLength: string; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip"); - const body = "x".repeat(10000); - contentLength = String(body.length); - const response = new Response( - body, - { - headers: { - "content-length": contentLength, - }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const cmd = [ - "curl", - "-i", - "--request", - "GET", - "--url", - url, - // "--compressed", // Windows curl does not support --compressed - "--header", - "Accept-Encoding: deflate, gzip", - ]; - // deno-lint-ignore no-deprecated-deno-api - const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); - const status = await proc.status(); - assert(status.success); - const output = decoder.decode(await proc.output()); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert(output.includes("content-encoding: gzip\r\n")); - // Ensure the content-length header is updated. - assert(!output.includes(`content-length: ${contentLength}\r\n`)); - assert(output.includes("content-length: ")); - proc.close(); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test({ - name: "http server custom content-encoding is left untouched", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - let contentLength: string; - - let httpConn: Deno.HttpConn; - async function server() { - const listener = Deno.listen({ hostname, port }); - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip"); - const body = new Uint8Array([3, 1, 4, 1]); - contentLength = String(body.length); - const response = new Response( - body, - { - headers: { - "content-length": contentLength, - "content-encoding": "arbitrary", - }, - }, - ); - await respondWith(response); - listener.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const cmd = [ - "curl", - "-i", - "--request", - "GET", - "--url", - url, - // "--compressed", // Windows curl does not support --compressed - "--header", - "Accept-Encoding: deflate, gzip", - ]; - // deno-lint-ignore no-deprecated-deno-api - const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); - const status = await proc.status(); - assert(status.success); - const output = decoder.decode(await proc.output()); - assert(output.includes("vary: Accept-Encoding\r\n")); - assert(output.includes("content-encoding: arbitrary\r\n")); - proc.close(); - } - - await Promise.all([server(), client()]); - httpConn!.close(); - }, -}); - -Deno.test( - { permissions: { net: true } }, - async function httpServerReadLargeBodyWithContentLength() { - const TLS_PACKET_SIZE = 16 * 1024 + 256; - // We want the body to be read in multiple packets - const body = "aa\n" + "deno.land large body\n".repeat(TLS_PACKET_SIZE) + - "zz"; - - let httpConn: Deno.HttpConn; - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const reqEvent = await httpConn.nextRequest(); - assert(reqEvent); - const { request, respondWith } = reqEvent; - assertEquals(await request.text(), body); - await respondWith(new Response(body)); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { - method: "POST", - headers: { "connection": "close" }, - body, - }); - const text = await resp.text(); - assertEquals(text, body); - await promise; - - httpConn!.close(); - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerReadLargeBodyWithTransferChunked() { - const TLS_PACKET_SIZE = 16 * 1024 + 256; - - // We want the body to be read in multiple packets - const chunks = [ - "aa\n", - "deno.land large body\n".repeat(TLS_PACKET_SIZE), - "zz", - ]; - - const body = chunks.join(""); - - const stream = new TransformStream(); - const writer = stream.writable.getWriter(); - for (const chunk of chunks) { - writer.write(new TextEncoder().encode(chunk)); - } - writer.close(); - - let httpConn: Deno.HttpConn; - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const reqEvent = await httpConn.nextRequest(); - assert(reqEvent); - const { request, respondWith } = reqEvent; - assertEquals(await request.text(), body); - await respondWith(new Response(body)); - })(); - - const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { - method: "POST", - headers: { "connection": "close" }, - body: stream.readable, - }); - const text = await resp.text(); - assertEquals(text, body); - await promise; - - httpConn!.close(); - }, -); - -Deno.test( - { - permissions: { net: true }, - }, - async function httpServerWithoutExclusiveAccessToTcp() { - const port = listenPort; - const listener = Deno.listen({ port }); - - const [clientConn, serverConn] = await Promise.all([ - Deno.connect({ port }), - listener.accept(), - ]); - - const buf = new Uint8Array(128); - const readPromise = serverConn.read(buf); - assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource); - - clientConn.close(); - listener.close(); - await readPromise; - }, -); - -Deno.test( - { - permissions: { net: true, read: true }, - }, - async function httpServerWithoutExclusiveAccessToTls() { - const hostname = "localhost"; - const port = listenPort; - const listener = Deno.listenTls({ - hostname, - port, - cert: await Deno.readTextFile("cli/tests/testdata/tls/localhost.crt"), - key: await Deno.readTextFile("cli/tests/testdata/tls/localhost.key"), - }); - - const caCerts = [ - await Deno.readTextFile("cli/tests/testdata/tls/RootCA.pem"), - ]; - const [clientConn, serverConn] = await Promise.all([ - Deno.connectTls({ hostname, port, caCerts }), - listener.accept(), - ]); - await Promise.all([clientConn.handshake(), serverConn.handshake()]); - - const buf = new Uint8Array(128); - const readPromise = serverConn.read(buf); - assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource); - - clientConn.close(); - listener.close(); - await readPromise; - }, -); - -Deno.test( - { - ignore: Deno.build.os === "windows", - permissions: { read: true, write: true }, - }, - async function httpServerWithoutExclusiveAccessToUnixSocket() { - const filePath = tmpUnixSocketPath(); - const listener = Deno.listen({ path: filePath, transport: "unix" }); - - const [clientConn, serverConn] = await Promise.all([ - Deno.connect({ path: filePath, transport: "unix" }), - listener.accept(), - ]); - - const buf = new Uint8Array(128); - const readPromise = serverConn.read(buf); - assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource); - - clientConn.close(); - listener.close(); - await readPromise; - }, -); - -Deno.test( - { permissions: { net: true } }, - async function httpServerRequestResponseClone() { - const body = "deno".repeat(64 * 1024); - let httpConn: Deno.HttpConn; - const listener = Deno.listen({ port: listenPort }); - const promise = (async () => { - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const reqEvent = await httpConn.nextRequest(); - assert(reqEvent); - const { request, respondWith } = reqEvent; - const clone = request.clone(); - const reader = clone.body!.getReader(); - - // get first chunk from branch2 - const clonedChunks = []; - const { value, done } = await reader.read(); - assert(!done); - clonedChunks.push(value); - - // consume request after first chunk single read - // readAll should read correctly the rest of the body. - // firstChunk should be in the stream internal buffer - const body1 = await request.text(); - - while (true) { - const { value, done } = await reader.read(); - if (done) break; - clonedChunks.push(value); - } - let offset = 0; - const body2 = new Uint8Array(body.length); - for (const chunk of clonedChunks) { - body2.set(chunk, offset); - offset += chunk.byteLength; - } - - assertEquals(body1, body); - assertEquals(body1, new TextDecoder().decode(body2)); - await respondWith(new Response(body)); - })(); - - const response = await fetch(`http://localhost:${listenPort}`, { - body, - method: "POST", - }); - const clone = response.clone(); - assertEquals(await response.text(), await clone.text()); - - await promise; - httpConn!.close(); - }, -); - -Deno.test({ - name: "http server compresses and flushes each chunk of a streamed resource", - permissions: { net: true, run: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - const port2 = listenPort2; - - const encoder = new TextEncoder(); - const listener = Deno.listen({ hostname, port }); - const listener2 = Deno.listen({ hostname, port: port2 }); - - let httpConn: Deno.HttpConn; - async function server() { - const tcpConn = await listener.accept(); - httpConn = Deno.serveHttp(tcpConn); - const e = await httpConn.nextRequest(); - assert(e); - const { request, respondWith } = e; - assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); - const resp = await fetch(`http://${hostname}:${port2}/`); - await respondWith(resp); - listener.close(); - } - - const ts = new TransformStream(); - const writer = ts.writable.getWriter(); - writer.write(encoder.encode("hello")); - - let httpConn2: Deno.HttpConn; - async function server2() { - const tcpConn = await listener2.accept(); - httpConn2 = Deno.serveHttp(tcpConn); - const e = await httpConn2.nextRequest(); - assert(e); - await e.respondWith( - new Response(ts.readable, { - headers: { "Content-Type": "text/plain" }, - }), - ); - listener2.close(); - } - - async function client() { - const url = `http://${hostname}:${port}/`; - const args = [ - "--request", - "GET", - "--url", - url, - "--header", - "Accept-Encoding: gzip, deflate, br", - "--no-buffer", - ]; - const proc = new Deno.Command("curl", { - args, - stderr: "null", - stdout: "piped", - }).spawn(); - const stdout = proc.stdout - .pipeThrough(new DecompressionStream("gzip")) - .pipeThrough(new TextDecoderStream()); - let body = ""; - for await (const chunk of stdout) { - body += chunk; - if (body === "hello") { - writer.write(encoder.encode(" world")); - writer.close(); - } - } - assertEquals(body, "hello world"); - const status = await proc.status; - assert(status.success); - } - - await Promise.all([server(), server2(), client()]); - httpConn!.close(); - httpConn2!.close(); - }, -}); - -Deno.test("case insensitive comma value finder", async (t) => { - const cases = /** @type {[string, boolean][]} */ ([ - ["websocket", true], - ["wEbSOcKET", true], - [",wEbSOcKET", true], - [",wEbSOcKET,", true], - [", wEbSOcKET ,", true], - ["test, wEbSOcKET ,", true], - ["test ,\twEbSOcKET\t\t ,", true], - ["test , wEbSOcKET", true], - ["test, asdf,web,wEbSOcKET", true], - ["test, asdf,web,wEbSOcKETs", false], - ["test, asdf,awebsocket,wEbSOcKETs", false], - ]); - - const findValue = buildCaseInsensitiveCommaValueFinder("websocket"); - for (const [input, expected] of cases) { - await t.step(input.toString(), () => { - const actual = findValue(input); - assertEquals(actual, expected); - }); - } -}); - -async function httpServerWithErrorBody( - listener: Deno.Listener, - compression: boolean, -): Promise<Deno.HttpConn> { - const conn = await listener.accept(); - listener.close(); - const httpConn = Deno.serveHttp(conn); - const e = await httpConn.nextRequest(); - assert(e); - const { respondWith } = e; - const originalErr = new Error("boom"); - const rs = new ReadableStream({ - async start(controller) { - controller.enqueue(new Uint8Array([65])); - await delay(1000); - controller.error(originalErr); - }, - }); - const init = compression ? { headers: { "content-type": "text/plain" } } : {}; - const response = new Response(rs, init); - const err = await assertRejects(() => respondWith(response)); - assert(err === originalErr); - return httpConn; -} - -for (const compression of [true, false]) { - Deno.test({ - name: `http server errors stream if response body errors (http/1.1${ - compression ? " + compression" : "" - })`, - permissions: { net: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - const listener = Deno.listen({ hostname, port }); - const server = httpServerWithErrorBody(listener, compression); - - const conn = await Deno.connect({ hostname, port }); - const msg = new TextEncoder().encode( - `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`, - ); - const nwritten = await conn.write(msg); - assertEquals(nwritten, msg.byteLength); - - const buf = new Uint8Array(1024); - const nread = await conn.read(buf); - assert(nread); - const data = new TextDecoder().decode(buf.subarray(0, nread)); - assert(data.endsWith("1\r\nA\r\n")); - const nread2 = await conn.read(buf); // connection should be closed now because the stream errored - assertEquals(nread2, null); - conn.close(); - - const httpConn = await server; - httpConn.close(); - }, - }); - - Deno.test({ - name: `http server errors stream if response body errors (http/1.1 + fetch${ - compression ? " + compression" : "" - })`, - permissions: { net: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - const listener = Deno.listen({ hostname, port }); - const server = httpServerWithErrorBody(listener, compression); - - const resp = await fetch(`http://${hostname}:${port}/`); - assert(resp.body); - const reader = resp.body.getReader(); - const result = await reader.read(); - assert(!result.done); - assertEquals(result.value, new Uint8Array([65])); - const err = await assertRejects(() => reader.read()); - assert(err instanceof TypeError); - assert(err.message.includes("unexpected EOF")); - - const httpConn = await server; - httpConn.close(); - }, - }); - - Deno.test({ - name: `http server errors stream if response body errors (http/2 + fetch${ - compression ? " + compression" : "" - }))`, - permissions: { net: true, read: true }, - async fn() { - const hostname = "localhost"; - const port = listenPort; - - const listener = Deno.listenTls({ - hostname, - port, - cert: await Deno.readTextFile("cli/tests/testdata/tls/localhost.crt"), - key: await Deno.readTextFile("cli/tests/testdata/tls/localhost.key"), - alpnProtocols: ["h2"], - }); - const server = httpServerWithErrorBody(listener, compression); - - const caCert = Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem"); - const client = Deno.createHttpClient({ caCerts: [caCert] }); - const resp = await fetch(`https://${hostname}:${port}/`, { client }); - client.close(); - assert(resp.body); - const reader = resp.body.getReader(); - const result = await reader.read(); - assert(!result.done); - assertEquals(result.value, new Uint8Array([65])); - const err = await assertRejects(() => reader.read()); - assert(err instanceof TypeError); - assert(err.message.includes("unexpected internal error encountered")); - - const httpConn = await server; - httpConn.close(); - }, - }); -} - -Deno.test({ - name: "request signal is aborted when response errors", - permissions: { net: true }, - async fn() { - let httpConn: Deno.HttpConn; - const promise = (async () => { - const listener = Deno.listen({ port: listenPort }); - const conn = await listener.accept(); - listener.close(); - httpConn = Deno.serveHttp(conn); - const ev = await httpConn.nextRequest(); - const { request, respondWith } = ev!; - - await delay(300); - await assertRejects(() => respondWith(new Response("Hello World"))); - assert(request.signal.aborted); - })(); - - const abortController = new AbortController(); - - fetch(`http://127.0.0.1:${listenPort}/`, { - signal: abortController.signal, - }).catch(() => { - // ignore - }); - - await delay(100); - abortController.abort(); - await promise; - httpConn!.close(); - }, -}); - -Deno.test( - async function httpConnExplicitResourceManagement() { - let promise; - - { - const listen = Deno.listen({ port: listenPort }); - promise = fetch(`http://localhost:${listenPort}/`).catch(() => null); - const serverConn = await listen.accept(); - listen.close(); - - using _httpConn = Deno.serveHttp(serverConn); - } - - const response = await promise; - assertEquals(response, null); - }, -); - -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 }; -} - -async function readTrailers( - headers: Headers, - r: BufReader, -) { - 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) => isProhibitedForTrailer(k)); - if (prohibited.length > 0) { - throw new Deno.errors.InvalidData( - `Prohibited trailer names: ${Deno.inspect(prohibited)}.`, - ); - } - return new Headers(trailerNames.map((key) => [key, ""])); -} - -function isProhibitedForTrailer(key: string): boolean { - const s = new Set(["transfer-encoding", "content-length", "trailer"]); - return s.has(key.toLowerCase()); -} |