diff options
author | Matt Mastracci <matthew@mastracci.com> | 2024-02-10 13:22:13 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-10 20:22:13 +0000 |
commit | f5e46c9bf2f50d66a953fa133161fc829cecff06 (patch) | |
tree | 8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/unit/tls_test.ts | |
parent | d2477f780630a812bfd65e3987b70c0d309385bb (diff) |
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests ->
tests, and updates of relative paths for files.
This is the first step towards aggregate all of the integration test
files under tests/, which will lead to a set of integration tests that
can run without the CLI binary being built.
While we could leave these tests under `cli`, it would require us to
keep a more complex directory structure for the various test runners. In
addition, we have a lot of complexity to ignore various test files in
the `cli` project itself (cargo publish exclusion rules, autotests =
false, etc).
And finally, the `tests/` folder will eventually house the `test_ffi`,
`test_napi` and other testing code, reducing the size of the root repo
directory.
For easier review, the extremely large and noisy "move" is in the first
commit (with no changes -- just a move), while the remainder of the
changes to actual files is in the second commit.
Diffstat (limited to 'tests/unit/tls_test.ts')
-rw-r--r-- | tests/unit/tls_test.ts | 1546 |
1 files changed, 1546 insertions, 0 deletions
diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts new file mode 100644 index 000000000..2bd7768bb --- /dev/null +++ b/tests/unit/tls_test.ts @@ -0,0 +1,1546 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertEquals, + assertNotEquals, + assertRejects, + assertStrictEquals, + assertThrows, +} from "./test_util.ts"; +import { BufReader, BufWriter } from "@test_util/std/io/mod.ts"; +import { readAll } from "@test_util/std/streams/read_all.ts"; +import { writeAll } from "@test_util/std/streams/write_all.ts"; +import { TextProtoReader } from "../testdata/run/textproto.ts"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const cert = await Deno.readTextFile("tests/testdata/tls/localhost.crt"); +const key = await Deno.readTextFile("tests/testdata/tls/localhost.key"); +const caCerts = [await Deno.readTextFile("tests/testdata/tls/RootCA.pem")]; + +async function sleep(msec: number) { + await new Promise((res, _rej) => setTimeout(res, msec)); +} + +function unreachable(): never { + throw new Error("Unreachable code reached"); +} + +Deno.test({ permissions: { net: false } }, async function connectTLSNoPerm() { + await assertRejects(async () => { + await Deno.connectTls({ hostname: "deno.land", port: 443 }); + }, Deno.errors.PermissionDenied); +}); + +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTLSInvalidHost() { + await assertRejects(async () => { + await Deno.connectTls({ hostname: "256.0.0.0", port: 3567 }); + }, TypeError); + }, +); + +Deno.test( + { permissions: { net: true, read: false } }, + async function connectTLSCertFileNoReadPerm() { + await assertRejects(async () => { + await Deno.connectTls({ + hostname: "deno.land", + port: 443, + certFile: "tests/testdata/tls/RootCA.crt", + }); + }, Deno.errors.PermissionDenied); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + function listenTLSNonExistentCertKeyFiles() { + const options = { + hostname: "localhost", + port: 3500, + certFile: "tests/testdata/tls/localhost.crt", + keyFile: "tests/testdata/tls/localhost.key", + }; + + assertThrows(() => { + Deno.listenTls({ + ...options, + certFile: "./non/existent/file", + }); + }, Deno.errors.NotFound); + + assertThrows(() => { + Deno.listenTls({ + ...options, + keyFile: "./non/existent/file", + }); + }, Deno.errors.NotFound); + }, +); + +Deno.test( + { permissions: { net: true, read: false } }, + function listenTLSNoReadPerm() { + assertThrows(() => { + Deno.listenTls({ + hostname: "localhost", + port: 3500, + certFile: "tests/testdata/tls/localhost.crt", + keyFile: "tests/testdata/tls/localhost.key", + }); + }, Deno.errors.PermissionDenied); + }, +); + +Deno.test( + { + permissions: { read: true, write: true, net: true }, + }, + function listenTLSEmptyKeyFile() { + const options = { + hostname: "localhost", + port: 3500, + certFile: "tests/testdata/tls/localhost.crt", + keyFile: "tests/testdata/tls/localhost.key", + }; + + const testDir = Deno.makeTempDirSync(); + const keyFilename = testDir + "/key.pem"; + Deno.writeFileSync(keyFilename, new Uint8Array([]), { + mode: 0o666, + }); + + assertThrows(() => { + Deno.listenTls({ + ...options, + keyFile: keyFilename, + }); + }, Error); + }, +); + +Deno.test( + { permissions: { read: true, write: true, net: true } }, + function listenTLSEmptyCertFile() { + const options = { + hostname: "localhost", + port: 3500, + certFile: "tests/testdata/tls/localhost.crt", + keyFile: "tests/testdata/tls/localhost.key", + }; + + const testDir = Deno.makeTempDirSync(); + const certFilename = testDir + "/cert.crt"; + Deno.writeFileSync(certFilename, new Uint8Array([]), { + mode: 0o666, + }); + + assertThrows(() => { + Deno.listenTls({ + ...options, + certFile: certFilename, + }); + }, Error); + }, +); + +Deno.test( + { permissions: { net: true } }, + async function startTlsWithoutExclusiveAccessToTcpConn() { + const hostname = "localhost"; + const port = getPort(); + + const tcpListener = Deno.listen({ hostname, port }); + const [serverConn, clientConn] = await Promise.all([ + tcpListener.accept(), + Deno.connect({ hostname, port }), + ]); + + const buf = new Uint8Array(128); + const readPromise = clientConn.read(buf); + // `clientConn` is being used by a pending promise (`readPromise`) so + // `Deno.startTls` cannot consume the connection. + await assertRejects( + () => Deno.startTls(clientConn, { hostname }), + Deno.errors.BadResource, + ); + + serverConn.close(); + tcpListener.close(); + await readPromise; + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function dialAndListenTLS() { + const { promise, resolve } = Promise.withResolvers<void>(); + const hostname = "localhost"; + const port = 3500; + + const listener = Deno.listenTls({ + hostname, + port, + cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"), + key: await Deno.readTextFile("tests/testdata/tls/localhost.key"), + }); + + const response = encoder.encode( + "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n", + ); + + listener.accept().then( + async (conn) => { + assert(conn.remoteAddr != null); + assert(conn.localAddr != null); + await conn.write(response); + // TODO(bartlomieju): this might be a bug + setTimeout(() => { + conn.close(); + resolve(); + }, 0); + }, + ); + + const conn = await Deno.connectTls({ hostname, port, caCerts }); + assert(conn.rid > 0); + const w = new BufWriter(conn); + const r = new BufReader(conn); + const body = `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\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, `line must be read: ${String(statusLine)}`); + const m = statusLine.match(/^(.+?) (.+?) (.+?)$/); + assert(m !== null, "must be matched"); + const [_, proto, status, ok] = m; + assertEquals(proto, "HTTP/1.1"); + assertEquals(status, "200"); + assertEquals(ok, "OK"); + const headers = await tpr.readMimeHeader(); + assert(headers !== null); + const contentLength = parseInt(headers.get("content-length")!); + const bodyBuf = new Uint8Array(contentLength); + await r.readFull(bodyBuf); + assertEquals(decoder.decode(bodyBuf), "Hello World\n"); + conn.close(); + listener.close(); + await promise; + }, +); +Deno.test( + { permissions: { read: false, net: true } }, + async function listenTlsWithCertAndKey() { + const { promise, resolve } = Promise.withResolvers<void>(); + const hostname = "localhost"; + const port = 3500; + + const listener = Deno.listenTls({ hostname, port, cert, key }); + + const response = encoder.encode( + "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n", + ); + + listener.accept().then( + async (conn) => { + assert(conn.remoteAddr != null); + assert(conn.localAddr != null); + await conn.write(response); + setTimeout(() => { + conn.close(); + resolve(); + }, 0); + }, + ); + + const conn = await Deno.connectTls({ hostname, port, caCerts }); + assert(conn.rid > 0); + const w = new BufWriter(conn); + const r = new BufReader(conn); + const body = `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\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, `line must be read: ${String(statusLine)}`); + const m = statusLine.match(/^(.+?) (.+?) (.+?)$/); + assert(m !== null, "must be matched"); + const [_, proto, status, ok] = m; + assertEquals(proto, "HTTP/1.1"); + assertEquals(status, "200"); + assertEquals(ok, "OK"); + const headers = await tpr.readMimeHeader(); + assert(headers !== null); + const contentLength = parseInt(headers.get("content-length")!); + const bodyBuf = new Uint8Array(contentLength); + await r.readFull(bodyBuf); + assertEquals(decoder.decode(bodyBuf), "Hello World\n"); + conn.close(); + listener.close(); + await promise; + }, +); + +let nextPort = 3501; +function getPort() { + return nextPort++; +} + +async function tlsPair(): Promise<[Deno.Conn, Deno.Conn]> { + const port = getPort(); + const listener = Deno.listenTls({ + hostname: "localhost", + port, + cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"), + key: await Deno.readTextFile("tests/testdata/tls/localhost.key"), + }); + + const acceptPromise = listener.accept(); + const connectPromise = Deno.connectTls({ + hostname: "localhost", + port, + caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")], + }); + const endpoints = await Promise.all([acceptPromise, connectPromise]); + + listener.close(); + + return endpoints; +} + +async function tlsAlpn( + useStartTls: boolean, +): Promise<[Deno.TlsConn, Deno.TlsConn]> { + const port = getPort(); + const listener = Deno.listenTls({ + hostname: "localhost", + port, + cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"), + key: await Deno.readTextFile("tests/testdata/tls/localhost.key"), + alpnProtocols: ["deno", "rocks"], + }); + + const acceptPromise = listener.accept(); + + const caCerts = [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")]; + const clientAlpnProtocols = ["rocks", "rises"]; + let endpoints: [Deno.TlsConn, Deno.TlsConn]; + + if (!useStartTls) { + const connectPromise = Deno.connectTls({ + hostname: "localhost", + port, + caCerts, + alpnProtocols: clientAlpnProtocols, + }); + endpoints = await Promise.all([acceptPromise, connectPromise]); + } else { + const client = await Deno.connect({ + hostname: "localhost", + port, + }); + const connectPromise = Deno.startTls(client, { + hostname: "localhost", + caCerts, + alpnProtocols: clientAlpnProtocols, + }); + endpoints = await Promise.all([acceptPromise, connectPromise]); + } + + listener.close(); + return endpoints; +} + +async function sendThenCloseWriteThenReceive( + conn: Deno.Conn, + chunkCount: number, + chunkSize: number, +) { + const byteCount = chunkCount * chunkSize; + const buf = new Uint8Array(chunkSize); // Note: buf is size of _chunk_. + let n: number; + + // Slowly send 42s. + buf.fill(42); + for (let remaining = byteCount; remaining > 0; remaining -= n) { + n = await conn.write(buf.subarray(0, remaining)); + assert(n >= 1); + await sleep(10); + } + + // Send EOF. + await conn.closeWrite(); + + // Receive 69s. + for (let remaining = byteCount; remaining > 0; remaining -= n) { + buf.fill(0); + n = await conn.read(buf) as number; + assert(n >= 1); + assertStrictEquals(buf[0], 69); + assertStrictEquals(buf[n - 1], 69); + } + + conn.close(); +} + +async function receiveThenSend( + conn: Deno.Conn, + chunkCount: number, + chunkSize: number, +) { + const byteCount = chunkCount * chunkSize; + const buf = new Uint8Array(byteCount); // Note: buf size equals `byteCount`. + let n: number; + + // Receive 42s. + for (let remaining = byteCount; remaining > 0; remaining -= n) { + buf.fill(0); + n = await conn.read(buf) as number; + assert(n >= 1); + assertStrictEquals(buf[0], 42); + assertStrictEquals(buf[n - 1], 42); + } + + // Slowly send 69s. + buf.fill(69); + for (let remaining = byteCount; remaining > 0; remaining -= n) { + n = await conn.write(buf.subarray(0, remaining)); + assert(n >= 1); + await sleep(10); + } + + conn.close(); +} + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerAlpnListenConnect() { + const [serverConn, clientConn] = await tlsAlpn(false); + const [serverHS, clientHS] = await Promise.all([ + serverConn.handshake(), + clientConn.handshake(), + ]); + assertStrictEquals(serverHS.alpnProtocol, "rocks"); + assertStrictEquals(clientHS.alpnProtocol, "rocks"); + + serverConn.close(); + clientConn.close(); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerAlpnListenStartTls() { + const [serverConn, clientConn] = await tlsAlpn(true); + const [serverHS, clientHS] = await Promise.all([ + serverConn.handshake(), + clientConn.handshake(), + ]); + assertStrictEquals(serverHS.alpnProtocol, "rocks"); + assertStrictEquals(clientHS.alpnProtocol, "rocks"); + + serverConn.close(); + clientConn.close(); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerStreamHalfCloseSendOneByte() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(serverConn, 1, 1), + receiveThenSend(clientConn, 1, 1), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsClientStreamHalfCloseSendOneByte() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(clientConn, 1, 1), + receiveThenSend(serverConn, 1, 1), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerStreamHalfCloseSendOneChunk() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(serverConn, 1, 1 << 20 /* 1 MB */), + receiveThenSend(clientConn, 1, 1 << 20 /* 1 MB */), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsClientStreamHalfCloseSendOneChunk() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(clientConn, 1, 1 << 20 /* 1 MB */), + receiveThenSend(serverConn, 1, 1 << 20 /* 1 MB */), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerStreamHalfCloseSendManyBytes() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(serverConn, 100, 1), + receiveThenSend(clientConn, 100, 1), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsClientStreamHalfCloseSendManyBytes() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(clientConn, 100, 1), + receiveThenSend(serverConn, 100, 1), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerStreamHalfCloseSendManyChunks() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(serverConn, 100, 1 << 16 /* 64 kB */), + receiveThenSend(clientConn, 100, 1 << 16 /* 64 kB */), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsClientStreamHalfCloseSendManyChunks() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(clientConn, 100, 1 << 16 /* 64 kB */), + receiveThenSend(serverConn, 100, 1 << 16 /* 64 kB */), + ]); + }, +); + +const largeAmount = 1 << 20 /* 1 MB */; + +async function sendAlotReceiveNothing(conn: Deno.Conn) { + // Start receive op. + const readBuf = new Uint8Array(1024); + const readPromise = conn.read(readBuf); + + const timeout = setTimeout(() => { + throw new Error("Failed to send buffer in a reasonable amount of time"); + }, 10_000); + + // Send 1 MB of data. + const writeBuf = new Uint8Array(largeAmount); + writeBuf.fill(42); + await writeAll(conn, writeBuf); + + clearTimeout(timeout); + + // Send EOF. + await conn.closeWrite(); + + // Close the connection. + conn.close(); + + // Read op should be canceled. + await assertRejects( + async () => await readPromise, + Deno.errors.Interrupted, + ); +} + +async function receiveAlotSendNothing(conn: Deno.Conn) { + const readBuf = new Uint8Array(1024); + let n: number | null; + let nread = 0; + + const timeout = setTimeout(() => { + throw new Error( + `Failed to read buffer in a reasonable amount of time (got ${nread}/${largeAmount})`, + ); + }, 10_000); + + // Receive 1 MB of data. + try { + for (; nread < largeAmount; nread += n!) { + n = await conn.read(readBuf); + assertStrictEquals(typeof n, "number"); + assert(n! > 0); + assertStrictEquals(readBuf[0], 42); + } + } catch (e) { + throw new Error( + `Got an error (${e.message}) after reading ${nread}/${largeAmount} bytes`, + { cause: e }, + ); + } + clearTimeout(timeout); + + // Close the connection, without sending anything at all. + conn.close(); +} + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerStreamCancelRead() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendAlotReceiveNothing(serverConn), + receiveAlotSendNothing(clientConn), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsClientStreamCancelRead() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendAlotReceiveNothing(clientConn), + receiveAlotSendNothing(serverConn), + ]); + }, +); + +async function sendReceiveEmptyBuf(conn: Deno.Conn) { + const byteBuf = new Uint8Array([1]); + const emptyBuf = new Uint8Array(0); + let n: number | null; + + n = await conn.write(emptyBuf); + assertStrictEquals(n, 0); + + n = await conn.read(emptyBuf); + assertStrictEquals(n, 0); + + n = await conn.write(byteBuf); + assertStrictEquals(n, 1); + + n = await conn.read(byteBuf); + assertStrictEquals(n, 1); + + await conn.closeWrite(); + + n = await conn.write(emptyBuf); + assertStrictEquals(n, 0); + + await assertRejects(async () => { + await conn.write(byteBuf); + }, Deno.errors.NotConnected); + + n = await conn.write(emptyBuf); + assertStrictEquals(n, 0); + + n = await conn.read(byteBuf); + assertStrictEquals(n, null); + + conn.close(); +} + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsStreamSendReceiveEmptyBuf() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendReceiveEmptyBuf(serverConn), + sendReceiveEmptyBuf(clientConn), + ]); + }, +); + +function immediateClose(conn: Deno.Conn) { + conn.close(); + return Promise.resolve(); +} + +async function closeWriteAndClose(conn: Deno.Conn) { + await conn.closeWrite(); + + if (await conn.read(new Uint8Array(1)) !== null) { + throw new Error("did not expect to receive data on TLS stream"); + } + + conn.close(); +} + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerStreamImmediateClose() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + immediateClose(serverConn), + closeWriteAndClose(clientConn), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsClientStreamImmediateClose() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + closeWriteAndClose(serverConn), + immediateClose(clientConn), + ]); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsClientAndServerStreamImmediateClose() { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + immediateClose(serverConn), + immediateClose(clientConn), + ]); + }, +); + +async function tlsWithTcpFailureTestImpl( + phase: "handshake" | "traffic", + cipherByteCount: number, + failureMode: "corruption" | "shutdown", + reverse: boolean, +) { + const tlsPort = getPort(); + const tlsListener = Deno.listenTls({ + hostname: "localhost", + port: tlsPort, + cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"), + key: await Deno.readTextFile("tests/testdata/tls/localhost.key"), + }); + + const tcpPort = getPort(); + const tcpListener = Deno.listen({ hostname: "localhost", port: tcpPort }); + + const [tlsServerConn, tcpServerConn] = await Promise.all([ + tlsListener.accept(), + Deno.connect({ hostname: "localhost", port: tlsPort }), + ]); + + const [tcpClientConn, tlsClientConn] = await Promise.all([ + tcpListener.accept(), + Deno.connectTls({ + hostname: "localhost", + port: tcpPort, + caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")], + }), + ]); + + tlsListener.close(); + tcpListener.close(); + + const { + tlsConn1, + tlsConn2, + tcpConn1, + tcpConn2, + } = reverse + ? { + tlsConn1: tlsClientConn, + tlsConn2: tlsServerConn, + tcpConn1: tcpClientConn, + tcpConn2: tcpServerConn, + } + : { + tlsConn1: tlsServerConn, + tlsConn2: tlsClientConn, + tcpConn1: tcpServerConn, + tcpConn2: tcpClientConn, + }; + + const tcpForwardingInterruptDeferred1 = Promise.withResolvers<void>(); + const tcpForwardingPromise1 = forwardBytes( + tcpConn2, + tcpConn1, + cipherByteCount, + tcpForwardingInterruptDeferred1, + ); + + const tcpForwardingInterruptDeferred2 = Promise.withResolvers<void>(); + const tcpForwardingPromise2 = forwardBytes( + tcpConn1, + tcpConn2, + Infinity, + tcpForwardingInterruptDeferred2, + ); + + switch (phase) { + case "handshake": { + let expectedError; + switch (failureMode) { + case "corruption": + expectedError = Deno.errors.InvalidData; + break; + case "shutdown": + expectedError = Deno.errors.UnexpectedEof; + break; + default: + unreachable(); + } + + const tlsTrafficPromise1 = Promise.all([ + assertRejects( + () => sendBytes(tlsConn1, 0x01, 1), + expectedError, + ), + assertRejects( + () => receiveBytes(tlsConn1, 0x02, 1), + expectedError, + ), + ]); + + const tlsTrafficPromise2 = Promise.all([ + assertRejects( + () => sendBytes(tlsConn2, 0x02, 1), + Deno.errors.UnexpectedEof, + ), + assertRejects( + () => receiveBytes(tlsConn2, 0x01, 1), + Deno.errors.UnexpectedEof, + ), + ]); + + await tcpForwardingPromise1; + + switch (failureMode) { + case "corruption": + await sendBytes(tcpConn1, 0xff, 1 << 14 /* 16 kB */); + break; + case "shutdown": + await tcpConn1.closeWrite(); + break; + default: + unreachable(); + } + await tlsTrafficPromise1; + + tcpForwardingInterruptDeferred2.resolve(); + await tcpForwardingPromise2; + await tcpConn2.closeWrite(); + await tlsTrafficPromise2; + + break; + } + + case "traffic": { + await Promise.all([ + sendBytes(tlsConn2, 0x88, 8888), + receiveBytes(tlsConn1, 0x88, 8888), + sendBytes(tlsConn1, 0x99, 99999), + receiveBytes(tlsConn2, 0x99, 99999), + ]); + + tcpForwardingInterruptDeferred1.resolve(); + await tcpForwardingInterruptDeferred1.promise; + + switch (failureMode) { + case "corruption": + await sendBytes(tcpConn1, 0xff, 1 << 14 /* 16 kB */); + await assertRejects( + () => receiveEof(tlsConn1), + Deno.errors.InvalidData, + ); + tcpForwardingInterruptDeferred2.resolve(); + break; + case "shutdown": + await Promise.all([ + tcpConn1.closeWrite(), + await assertRejects( + () => receiveEof(tlsConn1), + Deno.errors.UnexpectedEof, + ), + await tlsConn1.closeWrite(), + await receiveEof(tlsConn2), + ]); + break; + default: + unreachable(); + } + + await tcpForwardingPromise2; + + break; + } + + default: + unreachable(); + } + + tlsServerConn.close(); + tlsClientConn.close(); + tcpServerConn.close(); + tcpClientConn.close(); + + async function sendBytes( + conn: Deno.Conn, + byte: number, + count: number, + ) { + let buf = new Uint8Array(1 << 12 /* 4 kB */); + buf.fill(byte); + + while (count > 0) { + buf = buf.subarray(0, Math.min(buf.length, count)); + const nwritten = await conn.write(buf); + assertStrictEquals(nwritten, buf.length); + count -= nwritten; + } + } + + async function receiveBytes( + conn: Deno.Conn, + byte: number, + count: number, + ) { + let buf = new Uint8Array(1 << 12 /* 4 kB */); + while (count > 0) { + buf = buf.subarray(0, Math.min(buf.length, count)); + const r = await conn.read(buf); + assertNotEquals(r, null); + assert(buf.subarray(0, r!).every((b) => b === byte)); + count -= r!; + } + } + + async function receiveEof(conn: Deno.Conn) { + const buf = new Uint8Array(1); + const r = await conn.read(buf); + assertStrictEquals(r, null); + } + + async function forwardBytes( + source: Deno.Conn, + sink: Deno.Conn, + count: number, + interruptPromise: ReturnType<typeof Promise.withResolvers<void>>, + ) { + let buf = new Uint8Array(1 << 12 /* 4 kB */); + while (count > 0) { + buf = buf.subarray(0, Math.min(buf.length, count)); + const nread = await Promise.race([ + source.read(buf), + interruptPromise.promise, + ]); + if (nread == null) break; // Either EOF or interrupted. + const nwritten = await sink.write(buf.subarray(0, nread)); + assertStrictEquals(nread, nwritten); + count -= nwritten; + } + } +} + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsHandshakeWithTcpCorruptionImmediately() { + await tlsWithTcpFailureTestImpl("handshake", 0, "corruption", false); + await tlsWithTcpFailureTestImpl("handshake", 0, "corruption", true); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsHandshakeWithTcpShutdownImmediately() { + await tlsWithTcpFailureTestImpl("handshake", 0, "shutdown", false); + await tlsWithTcpFailureTestImpl("handshake", 0, "shutdown", true); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsHandshakeWithTcpCorruptionAfter70Bytes() { + await tlsWithTcpFailureTestImpl("handshake", 76, "corruption", false); + await tlsWithTcpFailureTestImpl("handshake", 78, "corruption", true); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsHandshakeWithTcpShutdownAfter70bytes() { + await tlsWithTcpFailureTestImpl("handshake", 77, "shutdown", false); + await tlsWithTcpFailureTestImpl("handshake", 79, "shutdown", true); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsHandshakeWithTcpCorruptionAfter200Bytes() { + await tlsWithTcpFailureTestImpl("handshake", 200, "corruption", false); + await tlsWithTcpFailureTestImpl("handshake", 202, "corruption", true); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsHandshakeWithTcpShutdownAfter200bytes() { + await tlsWithTcpFailureTestImpl("handshake", 201, "shutdown", false); + await tlsWithTcpFailureTestImpl("handshake", 203, "shutdown", true); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsTrafficWithTcpCorruption() { + await tlsWithTcpFailureTestImpl("traffic", Infinity, "corruption", false); + await tlsWithTcpFailureTestImpl("traffic", Infinity, "corruption", true); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsTrafficWithTcpShutdown() { + await tlsWithTcpFailureTestImpl("traffic", Infinity, "shutdown", false); + await tlsWithTcpFailureTestImpl("traffic", Infinity, "shutdown", true); + }, +); + +function createHttpsListener(port: number): Deno.Listener { + // Query format: `curl --insecure https://localhost:8443/z/12345` + // The server returns a response consisting of 12345 times the letter 'z'. + const listener = Deno.listenTls({ + hostname: "localhost", + port, + cert: Deno.readTextFileSync("./tests/testdata/tls/localhost.crt"), + key: Deno.readTextFileSync("./tests/testdata/tls/localhost.key"), + }); + + serve(listener); + return listener; + + async function serve(listener: Deno.Listener) { + for await (const conn of listener) { + const EOL = "\r\n"; + + // Read GET request plus headers. + const buf = new Uint8Array(1 << 12 /* 4 kB */); + const decoder = new TextDecoder(); + let req = ""; + while (!req.endsWith(EOL + EOL)) { + const n = await conn.read(buf); + if (n === null) throw new Error("Unexpected EOF"); + req += decoder.decode(buf.subarray(0, n)); + } + + // Parse GET request. + const { filler, count, version } = + /^GET \/(?<filler>[^\/]+)\/(?<count>\d+) HTTP\/(?<version>1\.\d)\r\n/ + .exec(req)!.groups as { + filler: string; + count: string; + version: string; + }; + + // Generate response. + const resBody = new TextEncoder().encode(filler.repeat(+count)); + const resHead = new TextEncoder().encode( + [ + `HTTP/${version} 200 OK`, + `Content-Length: ${resBody.length}`, + "Content-Type: text/plain", + ].join(EOL) + EOL + EOL, + ); + + // Send response. + await writeAll(conn, resHead); + await writeAll(conn, resBody); + + // Close TCP connection. + conn.close(); + } + } +} + +async function curl(url: string): Promise<string> { + const { success, code, stdout, stderr } = await new Deno.Command("curl", { + args: ["--insecure", url], + }).output(); + + if (!success) { + throw new Error( + `curl ${url} failed: ${code}:\n${new TextDecoder().decode(stderr)}`, + ); + } + return new TextDecoder().decode(stdout); +} + +Deno.test( + { permissions: { read: true, net: true, run: true } }, + async function curlFakeHttpsServer() { + const port = getPort(); + const listener = createHttpsListener(port); + + const res1 = await curl(`https://localhost:${port}/d/1`); + assertStrictEquals(res1, "d"); + + const res2 = await curl(`https://localhost:${port}/e/12345`); + assertStrictEquals(res2, "e".repeat(12345)); + + const count3 = 1 << 17; // 128 kB. + const res3 = await curl(`https://localhost:${port}/n/${count3}`); + assertStrictEquals(res3, "n".repeat(count3)); + + const count4 = 12345678; + const res4 = await curl(`https://localhost:${port}/o/${count4}`); + assertStrictEquals(res4, "o".repeat(count4)); + + listener.close(); + }, +); + +Deno.test( + // Ignored because gmail appears to reject us on CI sometimes + { ignore: true, permissions: { read: true, net: true } }, + async function startTls() { + const hostname = "smtp.gmail.com"; + const port = 587; + const encoder = new TextEncoder(); + + const conn = await Deno.connect({ + hostname, + port, + }); + + let writer = new BufWriter(conn); + let reader = new TextProtoReader(new BufReader(conn)); + + let line: string | null = (await reader.readLine()) as string; + assert(line.startsWith("220")); + + await writer.write(encoder.encode(`EHLO ${hostname}\r\n`)); + await writer.flush(); + + while ((line = (await reader.readLine()) as string)) { + assert(line.startsWith("250")); + if (line.startsWith("250 ")) break; + } + + await writer.write(encoder.encode("STARTTLS\r\n")); + await writer.flush(); + + line = await reader.readLine(); + + // Received the message that the server is ready to establish TLS + assertEquals(line, "220 2.0.0 Ready to start TLS"); + + const tlsConn = await Deno.startTls(conn, { hostname }); + writer = new BufWriter(tlsConn); + reader = new TextProtoReader(new BufReader(tlsConn)); + + // After that use TLS communication again + await writer.write(encoder.encode(`EHLO ${hostname}\r\n`)); + await writer.flush(); + + while ((line = (await reader.readLine()) as string)) { + assert(line.startsWith("250")); + if (line.startsWith("250 ")) break; + } + + tlsConn.close(); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTLSBadClientCertPrivateKey(): Promise<void> { + await assertRejects(async () => { + await Deno.connectTls({ + hostname: "deno.land", + port: 443, + certChain: "bad data", + privateKey: await Deno.readTextFile( + "tests/testdata/tls/localhost.key", + ), + }); + }, Deno.errors.InvalidData); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTLSBadPrivateKey(): Promise<void> { + await assertRejects(async () => { + await Deno.connectTls({ + hostname: "deno.land", + port: 443, + certChain: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + privateKey: "bad data", + }); + }, Deno.errors.InvalidData); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTLSNotPrivateKey(): Promise<void> { + await assertRejects(async () => { + await Deno.connectTls({ + hostname: "deno.land", + port: 443, + certChain: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + privateKey: "", + }); + }, Deno.errors.InvalidData); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function connectWithClientCert() { + // The test_server running on port 4552 responds with 'PASS' if client + // authentication was successful. Try it by running test_server and + // curl --key tests/testdata/tls/localhost.key \ + // --cert tests/testdata/tls/localhost.crt \ + // --cacert tests/testdata/tls/RootCA.crt https://localhost:4552/ + const conn = await Deno.connectTls({ + hostname: "localhost", + port: 4552, + certChain: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + privateKey: await Deno.readTextFile( + "tests/testdata/tls/localhost.key", + ), + caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")], + }); + const result = decoder.decode(await readAll(conn)); + assertEquals(result, "PASS"); + conn.close(); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTLSCaCerts() { + const conn = await Deno.connectTls({ + hostname: "localhost", + port: 4557, + caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")], + }); + const result = decoder.decode(await readAll(conn)); + assertEquals(result, "PASS"); + conn.close(); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTLSCertFile() { + const conn = await Deno.connectTls({ + hostname: "localhost", + port: 4557, + certFile: "tests/testdata/tls/RootCA.pem", + }); + const result = decoder.decode(await readAll(conn)); + assertEquals(result, "PASS"); + conn.close(); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function startTLSCaCerts() { + const plainConn = await Deno.connect({ + hostname: "localhost", + port: 4557, + }); + const conn = await Deno.startTls(plainConn, { + hostname: "localhost", + caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")], + }); + const result = decoder.decode(await readAll(conn)); + assertEquals(result, "PASS"); + conn.close(); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsHandshakeSuccess() { + const hostname = "localhost"; + const port = getPort(); + + const listener = Deno.listenTls({ + hostname, + port, + cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"), + key: await Deno.readTextFile("tests/testdata/tls/localhost.key"), + }); + const acceptPromise = listener.accept(); + const connectPromise = Deno.connectTls({ + hostname, + port, + certFile: "tests/testdata/tls/RootCA.crt", + }); + const [conn1, conn2] = await Promise.all([acceptPromise, connectPromise]); + listener.close(); + + await Promise.all([conn1.handshake(), conn2.handshake()]); + + // Begin sending a 10mb blob over the TLS connection. + const whole = new Uint8Array(10 << 20); // 10mb. + whole.fill(42); + const sendPromise = writeAll(conn1, whole); + // Set up the other end to receive half of the large blob. + const half = new Uint8Array(whole.byteLength / 2); + const receivePromise = readFull(conn2, half); + + await conn1.handshake(); + await conn2.handshake(); + + // Finish receiving the first 5mb. + assertEquals(await receivePromise, half.length); + + // See that we can call `handshake()` in the middle of large reads and writes. + await conn1.handshake(); + await conn2.handshake(); + + // Receive second half of large blob. Wait for the send promise and check it. + assertEquals(await readFull(conn2, half), half.length); + await sendPromise; + + await conn1.handshake(); + await conn2.handshake(); + + await conn1.closeWrite(); + await conn2.closeWrite(); + + await conn1.handshake(); + await conn2.handshake(); + + conn1.close(); + conn2.close(); + + async function readFull(conn: Deno.Conn, buf: Uint8Array) { + let offset, n; + for (offset = 0; offset < buf.length; offset += n) { + n = await conn.read(buf.subarray(offset, buf.length)); + assert(n != null && n > 0); + } + return offset; + } + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsHandshakeFailure() { + const hostname = "localhost"; + const port = getPort(); + + async function server() { + const listener = Deno.listenTls({ + hostname, + port, + cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"), + key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"), + }); + for await (const conn of listener) { + for (let i = 0; i < 10; i++) { + // Handshake fails because the client rejects the server certificate. + await assertRejects( + () => conn.handshake(), + Deno.errors.InvalidData, + "received fatal alert", + ); + } + conn.close(); + break; + } + } + + async function connectTlsClient() { + const conn = await Deno.connectTls({ hostname, port }); + // Handshake fails because the server presents a self-signed certificate. + await assertRejects( + () => conn.handshake(), + Deno.errors.InvalidData, + "invalid peer certificate: UnknownIssuer", + ); + conn.close(); + } + + await Promise.all([server(), connectTlsClient()]); + + async function startTlsClient() { + const tcpConn = await Deno.connect({ hostname, port }); + const tlsConn = await Deno.startTls(tcpConn, { + hostname: "foo.land", + caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")], + }); + // Handshake fails because hostname doesn't match the certificate. + await assertRejects( + () => tlsConn.handshake(), + Deno.errors.InvalidData, + "NotValidForName", + ); + tlsConn.close(); + } + + await Promise.all([server(), startTlsClient()]); + }, +); + +Deno.test( + { permissions: { net: true } }, + async function listenTlsWithReuseAddr() { + const deferred1 = Promise.withResolvers<void>(); + const hostname = "localhost"; + const port = 3500; + + const listener1 = Deno.listenTls({ hostname, port, cert, key }); + + listener1.accept().then((conn) => { + conn.close(); + deferred1.resolve(); + }); + + const conn1 = await Deno.connectTls({ hostname, port, caCerts }); + conn1.close(); + await deferred1.promise; + listener1.close(); + + const deferred2 = Promise.withResolvers<void>(); + const listener2 = Deno.listenTls({ hostname, port, cert, key }); + + listener2.accept().then((conn) => { + conn.close(); + deferred2.resolve(); + }); + + const conn2 = await Deno.connectTls({ hostname, port, caCerts }); + conn2.close(); + await deferred2.promise; + listener2.close(); + }, +); + +Deno.test({ + ignore: Deno.build.os !== "linux", + permissions: { net: true }, +}, async function listenTlsReusePort() { + const hostname = "localhost"; + const port = 4003; + const listener1 = Deno.listenTls({ + hostname, + port, + cert, + key, + reusePort: true, + }); + const listener2 = Deno.listenTls({ + hostname, + port, + cert, + key, + reusePort: true, + }); + let p1; + let p2; + let listener1Recv = false; + let listener2Recv = false; + while (!listener1Recv || !listener2Recv) { + if (!p1) { + p1 = listener1.accept().then((conn) => { + conn.close(); + listener1Recv = true; + p1 = undefined; + }).catch(() => {}); + } + if (!p2) { + p2 = listener2.accept().then((conn) => { + conn.close(); + listener2Recv = true; + p2 = undefined; + }).catch(() => {}); + } + const conn = await Deno.connectTls({ hostname, port, caCerts }); + conn.close(); + await Promise.race([p1, p2]); + } + listener1.close(); + listener2.close(); +}); + +Deno.test({ + ignore: Deno.build.os === "linux", + permissions: { net: true }, +}, function listenTlsReusePortDoesNothing() { + const hostname = "localhost"; + const port = 4003; + const listener1 = Deno.listenTls({ + hostname, + port, + cert, + key, + reusePort: true, + }); + assertThrows(() => { + Deno.listenTls({ hostname, port, cert, key, reusePort: true }); + }, Deno.errors.AddrInUse); + listener1.close(); +}); + +Deno.test({ + permissions: { net: true }, +}, function listenTlsDoesNotThrowOnStringPort() { + const listener = Deno.listenTls({ + hostname: "localhost", + // @ts-ignore String port is not allowed by typing, but it shouldn't throw + // for backwards compatibility. + port: "0", + cert, + key, + }); + listener.close(); +}); + +Deno.test( + { permissions: { net: true, read: true } }, + function listenTLSInvalidCert() { + assertThrows(() => { + Deno.listenTls({ + hostname: "localhost", + port: 3500, + certFile: "tests/testdata/tls/invalid.crt", + keyFile: "tests/testdata/tls/localhost.key", + }); + }, Deno.errors.InvalidData); + }, +); + +Deno.test( + { permissions: { net: true, read: true } }, + function listenTLSInvalidKey() { + assertThrows(() => { + Deno.listenTls({ + hostname: "localhost", + port: 3500, + certFile: "tests/testdata/tls/localhost.crt", + keyFile: "tests/testdata/tls/invalid.key", + }); + }, Deno.errors.InvalidData); + }, +); |