// 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 "@std/io/mod.ts"; import { readAll } from "@std/io/read_all.ts"; import { writeAll } from "@std/io/write_all.ts"; import { TextProtoReader } from "../testdata/run/textproto.ts"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); const cert = Deno.readTextFileSync("tests/testdata/tls/localhost.crt"); const key = Deno.readTextFileSync("tests/testdata/tls/localhost.key"); const caCerts = [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")]; async function sleep(msec: number) { await new Promise((res, _rej) => setTimeout(res, msec)); } function listenTls( options?: { alpnProtocols?: string[]; reusePort?: boolean }, ): { listener: Deno.TlsListener; port: number; hostname: string } { const tlsOptions = { port: 0, hostname: "localhost", cert, key, ...options }; const listener = Deno.listenTls(tlsOptions); return { listener, port: ( listener.addr).port, hostname: "localhost", }; } function listenTcp(): { listener: Deno.Listener; port: number; hostname: string; } { const listener = Deno.listen({ port: 0, hostname: "localhost" }); return { listener, port: ( listener.addr).port, hostname: "localhost", }; } 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: 0, 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: 0, 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: 0, 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: 0, 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 { listener, hostname, port } = listenTcp(); const [serverConn, clientConn] = await Promise.all([ listener.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(); listener.close(); await readPromise; }, ); Deno.test( { permissions: { read: true, net: true } }, async function dialAndListenTLS() { const { promise, resolve } = Promise.withResolvers(); const { listener, port, hostname } = listenTls(); 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(); const { listener, hostname, port } = listenTls(); 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; }, ); async function tlsPair(): Promise<[Deno.Conn, Deno.Conn]> { const { listener, hostname, port } = listenTls(); const acceptPromise = listener.accept(); const connectPromise = Deno.connectTls({ hostname, 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 { listener, port } = listenTls({ 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 tls = listenTls(); const tcp = listenTcp(); const [tlsServerConn, tcpServerConn] = await Promise.all([ tls.listener.accept(), Deno.connect({ hostname: tls.hostname, port: tls.port }), ]); const [tcpClientConn, tlsClientConn] = await Promise.all([ tcp.listener.accept(), Deno.connectTls({ hostname: tcp.hostname, port: tcp.port, caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")], }), ]); tls.listener.close(); tcp.listener.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(); const tcpForwardingPromise1 = forwardBytes( tcpConn2, tcpConn1, cipherByteCount, tcpForwardingInterruptDeferred1, ); const tcpForwardingInterruptDeferred2 = Promise.withResolvers(); 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>, ) { 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(): { listener: Deno.TlsListener; hostname: string; port: number; } { // Query format: `curl --insecure https://localhost:8443/z/12345` // The server returns a response consisting of 12345 times the letter 'z'. const { listener, hostname, port } = listenTls(); serve(listener); return { listener, hostname, port }; 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 \/(?[^\/]+)\/(?\d+) HTTP\/(?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 { 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 { listener, port } = createHttpsListener(); 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 { await assertRejects(async () => { await Deno.connectTls({ hostname: "deno.land", port: 443, certChain: "bad data", privateKey: Deno.readTextFileSync( "tests/testdata/tls/localhost.key", ), }); }, Deno.errors.InvalidData); }, ); Deno.test( { permissions: { read: true, net: true } }, async function connectTLSBadCertKey(): Promise { await assertRejects(async () => { await Deno.connectTls({ hostname: "deno.land", port: 443, cert: "bad data", key: Deno.readTextFileSync( "tests/testdata/tls/localhost.key", ), }); }, Deno.errors.InvalidData); }, ); Deno.test( { permissions: { read: true, net: true } }, async function connectTLSBadPrivateKey(): Promise { await assertRejects(async () => { await Deno.connectTls({ hostname: "deno.land", port: 443, certChain: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), privateKey: "bad data", }); }, Deno.errors.InvalidData); }, ); Deno.test( { permissions: { read: true, net: true } }, async function connectTLSBadKey(): Promise { await assertRejects(async () => { await Deno.connectTls({ hostname: "deno.land", port: 443, cert: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), key: "bad data", }); }, Deno.errors.InvalidData); }, ); Deno.test( { permissions: { read: true, net: true } }, async function connectTLSNotPrivateKey(): Promise { await assertRejects(async () => { await Deno.connectTls({ hostname: "deno.land", port: 443, certChain: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), privateKey: "", }); }, Deno.errors.InvalidData); }, ); Deno.test( { permissions: { read: true, net: true } }, async function connectTLSNotKey(): Promise { await assertRejects(async () => { await Deno.connectTls({ hostname: "deno.land", port: 443, cert: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), key: "", }); }, 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: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), privateKey: Deno.readTextFileSync( "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 connectWithCert() { // The test_server running on port 4552 responds with 'PASS' if client // authentication was successful. Try it by running test_server and // curl --key cli/tests/testdata/tls/localhost.key \ // --cert cli/tests/testdata/tls/localhost.crt \ // --cacert cli/tests/testdata/tls/RootCA.crt https://localhost:4552/ const conn = await Deno.connectTls({ hostname: "localhost", port: 4552, cert: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), key: Deno.readTextFileSync( "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 connectTlsConflictingCertOptions(): Promise { await assertRejects( async () => { await Deno.connectTls({ hostname: "deno.land", port: 443, cert: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), certChain: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), key: Deno.readTextFileSync( "tests/testdata/tls/localhost.key", ), }); }, TypeError, "Cannot specify both `certChain` and `cert`", ); }, ); Deno.test( { permissions: { read: true, net: true } }, async function connectTlsConflictingKeyOptions(): Promise { await assertRejects( async () => { await Deno.connectTls({ hostname: "deno.land", port: 443, cert: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), privateKey: Deno.readTextFileSync( "tests/testdata/tls/localhost.crt", ), key: Deno.readTextFileSync( "tests/testdata/tls/localhost.key", ), }); }, TypeError, "Cannot specify both `privateKey` and `key`", ); }, ); 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 { listener, hostname, port } = listenTls(); 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() { let tls: { listener: Deno.TlsListener; port: number; hostname: string }; async function server() { for await (const conn of tls.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: tls.hostname, port: tls.port, }); // Handshake fails because the server presents a self-signed certificate. await assertRejects( () => conn.handshake(), Deno.errors.InvalidData, "invalid peer certificate: UnknownIssuer", ); conn.close(); } tls = listenTls(); await Promise.all([server(), connectTlsClient()]); async function startTlsClient() { const tcpConn = await Deno.connect({ hostname: tls.hostname, port: tls.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(); } tls = listenTls(); await Promise.all([server(), startTlsClient()]); }, ); Deno.test( { permissions: { net: true } }, async function listenTlsWithReuseAddr() { const deferred1 = Promise.withResolvers(); const { listener: listener1, port, hostname } = listenTls(); 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(); 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 { listener: listener1, port, hostname } = listenTls({ 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 { listener: listener1, hostname, port } = listenTls({ 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: 0, 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: 0, certFile: "tests/testdata/tls/localhost.crt", keyFile: "tests/testdata/tls/invalid.key", }); }, Deno.errors.InvalidData); }, );