diff options
Diffstat (limited to 'cli/tests')
-rw-r--r-- | cli/tests/integration_tests.rs | 165 | ||||
-rw-r--r-- | cli/tests/unit/tls_test.ts | 685 |
2 files changed, 733 insertions, 117 deletions
diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 56043930b..5cca0d1cc 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -5,7 +5,9 @@ use deno_core::serde_json; use deno_core::url; use deno_runtime::deno_fetch::reqwest; use deno_runtime::deno_websocket::tokio_tungstenite; -use rustls::Session; +use deno_runtime::ops::tls::rustls; +use deno_runtime::ops::tls::webpki; +use deno_runtime::ops::tls::TlsStream; use std::fs; use std::io::BufReader; use std::io::Cursor; @@ -14,8 +16,7 @@ use std::process::Command; use std::sync::Arc; use tempfile::TempDir; use test_util as util; -use tokio_rustls::rustls; -use tokio_rustls::webpki; +use tokio::task::LocalSet; #[test] fn js_unit_tests_lint() { @@ -6134,79 +6135,103 @@ console.log("finish"); #[tokio::test] async fn listen_tls_alpn() { - let child = util::deno_cmd() - .current_dir(util::root_path()) - .arg("run") - .arg("--unstable") - .arg("--quiet") - .arg("--allow-net") - .arg("--allow-read") - .arg("./cli/tests/listen_tls_alpn.ts") - .arg("4504") - .stdout(std::process::Stdio::piped()) - .spawn() - .unwrap(); - let mut stdout = child.stdout.unwrap(); - let mut buffer = [0; 5]; - let read = stdout.read(&mut buffer).unwrap(); - assert_eq!(read, 5); - let msg = std::str::from_utf8(&buffer).unwrap(); - assert_eq!(msg, "READY"); - - let mut cfg = rustls::ClientConfig::new(); - let reader = - &mut BufReader::new(Cursor::new(include_bytes!("./tls/RootCA.crt"))); - cfg.root_store.add_pem_file(reader).unwrap(); - cfg.alpn_protocols.push("foobar".as_bytes().to_vec()); - - let tls_connector = tokio_rustls::TlsConnector::from(Arc::new(cfg)); - let hostname = webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap(); - let stream = tokio::net::TcpStream::connect("localhost:4504") - .await - .unwrap(); + // TLS streams require the presence of an ambient local task set to gracefully + // close dropped connections in the background. + LocalSet::new() + .run_until(async { + let mut child = util::deno_cmd() + .current_dir(util::root_path()) + .arg("run") + .arg("--unstable") + .arg("--quiet") + .arg("--allow-net") + .arg("--allow-read") + .arg("./cli/tests/listen_tls_alpn.ts") + .arg("4504") + .stdout(std::process::Stdio::piped()) + .spawn() + .unwrap(); + let stdout = child.stdout.as_mut().unwrap(); + let mut buffer = [0; 5]; + let read = stdout.read(&mut buffer).unwrap(); + assert_eq!(read, 5); + let msg = std::str::from_utf8(&buffer).unwrap(); + assert_eq!(msg, "READY"); + + let mut cfg = rustls::ClientConfig::new(); + let reader = + &mut BufReader::new(Cursor::new(include_bytes!("./tls/RootCA.crt"))); + cfg.root_store.add_pem_file(reader).unwrap(); + cfg.alpn_protocols.push("foobar".as_bytes().to_vec()); + let cfg = Arc::new(cfg); + + let hostname = + webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap(); + + let tcp_stream = tokio::net::TcpStream::connect("localhost:4504") + .await + .unwrap(); + let mut tls_stream = + TlsStream::new_client_side(tcp_stream, &cfg, hostname); + tls_stream.handshake().await.unwrap(); + let (_, session) = tls_stream.get_ref(); - let tls_stream = tls_connector.connect(hostname, stream).await.unwrap(); - let (_, session) = tls_stream.get_ref(); + let alpn = session.get_alpn_protocol().unwrap(); + assert_eq!(std::str::from_utf8(alpn).unwrap(), "foobar"); - let alpn = session.get_alpn_protocol().unwrap(); - assert_eq!(std::str::from_utf8(alpn).unwrap(), "foobar"); + child.kill().unwrap(); + child.wait().unwrap(); + }) + .await; } #[tokio::test] async fn listen_tls_alpn_fail() { - let child = util::deno_cmd() - .current_dir(util::root_path()) - .arg("run") - .arg("--unstable") - .arg("--quiet") - .arg("--allow-net") - .arg("--allow-read") - .arg("./cli/tests/listen_tls_alpn.ts") - .arg("4505") - .stdout(std::process::Stdio::piped()) - .spawn() - .unwrap(); - let mut stdout = child.stdout.unwrap(); - let mut buffer = [0; 5]; - let read = stdout.read(&mut buffer).unwrap(); - assert_eq!(read, 5); - let msg = std::str::from_utf8(&buffer).unwrap(); - assert_eq!(msg, "READY"); - - let mut cfg = rustls::ClientConfig::new(); - let reader = - &mut BufReader::new(Cursor::new(include_bytes!("./tls/RootCA.crt"))); - cfg.root_store.add_pem_file(reader).unwrap(); - cfg.alpn_protocols.push("boofar".as_bytes().to_vec()); - - let tls_connector = tokio_rustls::TlsConnector::from(Arc::new(cfg)); - let hostname = webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap(); - let stream = tokio::net::TcpStream::connect("localhost:4505") - .await - .unwrap(); + // TLS streams require the presence of an ambient local task set to gracefully + // close dropped connections in the background. + LocalSet::new() + .run_until(async { + let mut child = util::deno_cmd() + .current_dir(util::root_path()) + .arg("run") + .arg("--unstable") + .arg("--quiet") + .arg("--allow-net") + .arg("--allow-read") + .arg("./cli/tests/listen_tls_alpn.ts") + .arg("4505") + .stdout(std::process::Stdio::piped()) + .spawn() + .unwrap(); + let stdout = child.stdout.as_mut().unwrap(); + let mut buffer = [0; 5]; + let read = stdout.read(&mut buffer).unwrap(); + assert_eq!(read, 5); + let msg = std::str::from_utf8(&buffer).unwrap(); + assert_eq!(msg, "READY"); + + let mut cfg = rustls::ClientConfig::new(); + let reader = + &mut BufReader::new(Cursor::new(include_bytes!("./tls/RootCA.crt"))); + cfg.root_store.add_pem_file(reader).unwrap(); + cfg.alpn_protocols.push("boofar".as_bytes().to_vec()); + let cfg = Arc::new(cfg); + + let hostname = + webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap(); + + let tcp_stream = tokio::net::TcpStream::connect("localhost:4505") + .await + .unwrap(); + let mut tls_stream = + TlsStream::new_client_side(tcp_stream, &cfg, hostname); + tls_stream.handshake().await.unwrap(); + let (_, session) = tls_stream.get_ref(); - let tls_stream = tls_connector.connect(hostname, stream).await.unwrap(); - let (_, session) = tls_stream.get_ref(); + assert!(session.get_alpn_protocol().is_none()); - assert!(session.get_alpn_protocol().is_none()); + child.kill().unwrap(); + child.wait().unwrap(); + }) + .await; } diff --git a/cli/tests/unit/tls_test.ts b/cli/tests/unit/tls_test.ts index 0528c8043..cedcf467d 100644 --- a/cli/tests/unit/tls_test.ts +++ b/cli/tests/unit/tls_test.ts @@ -2,9 +2,11 @@ import { assert, assertEquals, + assertNotEquals, assertStrictEquals, assertThrows, assertThrowsAsync, + Deferred, deferred, unitTest, } from "./test_util.ts"; @@ -14,6 +16,14 @@ import { TextProtoReader } from "../../../test_util/std/textproto/mod.ts"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); +async function sleep(msec: number): Promise<void> { + await new Promise((res, _rej) => setTimeout(res, msec)); +} + +function unreachable(): never { + throw new Error("Unreachable code reached"); +} + unitTest(async function connectTLSNoPerm(): Promise<void> { await assertThrowsAsync(async () => { await Deno.connectTls({ hostname: "github.com", port: 443 }); @@ -201,7 +211,13 @@ unitTest( }, ); -async function tlsPair(port: number): Promise<[Deno.Conn, Deno.Conn]> { +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, @@ -215,59 +231,169 @@ async function tlsPair(port: number): Promise<[Deno.Conn, Deno.Conn]> { port, certFile: "cli/tests/tls/RootCA.pem", }); - const connections = await Promise.all([acceptPromise, connectPromise]); + const endpoints = await Promise.all([acceptPromise, connectPromise]); listener.close(); - return connections; + return endpoints; } -async function sendCloseWrite(conn: Deno.Conn): Promise<void> { - const buf = new Uint8Array(1024); - let n: number | null; - - // Send 1. - n = await conn.write(new Uint8Array([1])); - assertStrictEquals(n, 1); +async function sendThenCloseWriteThenReceive( + conn: Deno.Conn, + chunkCount: number, + chunkSize: number, +): Promise<void> { + 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 2. - n = await conn.read(buf); - assertStrictEquals(n, 1); - assertStrictEquals(buf[0], 2); + // 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 receiveCloseWrite(conn: Deno.Conn): Promise<void> { - const buf = new Uint8Array(1024); - let n: number | null; - - // Receive 1. - n = await conn.read(buf); - assertStrictEquals(n, 1); - assertStrictEquals(buf[0], 1); - - // Receive EOF. - n = await conn.read(buf); - assertStrictEquals(n, null); +async function receiveThenSend( + conn: Deno.Conn, + chunkCount: number, + chunkSize: number, +): Promise<void> { + 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); + } - // Send 2. - n = await conn.write(new Uint8Array([2])); - assertStrictEquals(n, 1); + // 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(); } +unitTest( + { perms: { read: true, net: true } }, + async function tlsServerStreamHalfCloseSendOneByte(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(serverConn, 1, 1), + receiveThenSend(clientConn, 1, 1), + ]); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsClientStreamHalfCloseSendOneByte(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(clientConn, 1, 1), + receiveThenSend(serverConn, 1, 1), + ]); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsServerStreamHalfCloseSendOneChunk(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(serverConn, 1, 1 << 20 /* 1 MB */), + receiveThenSend(clientConn, 1, 1 << 20 /* 1 MB */), + ]); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsClientStreamHalfCloseSendOneChunk(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(clientConn, 1, 1 << 20 /* 1 MB */), + receiveThenSend(serverConn, 1, 1 << 20 /* 1 MB */), + ]); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsServerStreamHalfCloseSendManyBytes(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(serverConn, 100, 1), + receiveThenSend(clientConn, 100, 1), + ]); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsClientStreamHalfCloseSendManyBytes(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(clientConn, 100, 1), + receiveThenSend(serverConn, 100, 1), + ]); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsServerStreamHalfCloseSendManyChunks(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(serverConn, 100, 1 << 16 /* 64 kB */), + receiveThenSend(clientConn, 100, 1 << 16 /* 64 kB */), + ]); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsClientStreamHalfCloseSendManyChunks(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + sendThenCloseWriteThenReceive(clientConn, 100, 1 << 16 /* 64 kB */), + receiveThenSend(serverConn, 100, 1 << 16 /* 64 kB */), + ]); + }, +); + async function sendAlotReceiveNothing(conn: Deno.Conn): Promise<void> { // Start receive op. const readBuf = new Uint8Array(1024); const readPromise = conn.read(readBuf); // Send 1 MB of data. - const writeBuf = new Uint8Array(1 << 20); + const writeBuf = new Uint8Array(1 << 20 /* 1 MB */); writeBuf.fill(42); await conn.write(writeBuf); @@ -289,7 +415,7 @@ async function receiveAlotSendNothing(conn: Deno.Conn): Promise<void> { let n: number | null; // Receive 1 MB of data. - for (let nread = 0; nread < 1 << 20; nread += n!) { + for (let nread = 0; nread < 1 << 20 /* 1 MB */; nread += n!) { n = await conn.read(readBuf); assertStrictEquals(typeof n, "number"); assert(n! > 0); @@ -302,50 +428,515 @@ async function receiveAlotSendNothing(conn: Deno.Conn): Promise<void> { unitTest( { perms: { read: true, net: true } }, - async function tlsServerStreamHalfClose(): Promise<void> { - const [serverConn, clientConn] = await tlsPair(3501); + async function tlsServerStreamCancelRead(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); await Promise.all([ - sendCloseWrite(serverConn), - receiveCloseWrite(clientConn), + sendAlotReceiveNothing(serverConn), + receiveAlotSendNothing(clientConn), ]); }, ); unitTest( { perms: { read: true, net: true } }, - async function tlsClientStreamHalfClose(): Promise<void> { - const [serverConn, clientConn] = await tlsPair(3502); + async function tlsClientStreamCancelRead(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); await Promise.all([ - sendCloseWrite(clientConn), - receiveCloseWrite(serverConn), + sendAlotReceiveNothing(clientConn), + receiveAlotSendNothing(serverConn), ]); }, ); +async function sendReceiveEmptyBuf(conn: Deno.Conn): Promise<void> { + 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 assertThrowsAsync(async () => { + await conn.write(byteBuf); + }, Deno.errors.BrokenPipe); + + n = await conn.write(emptyBuf); + assertStrictEquals(n, 0); + + n = await conn.read(byteBuf); + assertStrictEquals(n, null); + + conn.close(); +} + unitTest( { perms: { read: true, net: true } }, - async function tlsServerStreamCancelRead(): Promise<void> { - const [serverConn, clientConn] = await tlsPair(3503); + async function tlsStreamSendReceiveEmptyBuf(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); await Promise.all([ - sendAlotReceiveNothing(serverConn), - receiveAlotSendNothing(clientConn), + sendReceiveEmptyBuf(serverConn), + sendReceiveEmptyBuf(clientConn), ]); }, ); +function immediateClose(conn: Deno.Conn): Promise<void> { + conn.close(); + return Promise.resolve(); +} + +async function closeWriteAndClose(conn: Deno.Conn): Promise<void> { + 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(); +} + unitTest( { perms: { read: true, net: true } }, - async function tlsClientStreamCancelRead(): Promise<void> { - const [serverConn, clientConn] = await tlsPair(3504); + async function tlsServerStreamImmediateClose(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); await Promise.all([ - sendAlotReceiveNothing(clientConn), - receiveAlotSendNothing(serverConn), + immediateClose(serverConn), + closeWriteAndClose(clientConn), ]); }, ); unitTest( { perms: { read: true, net: true } }, + async function tlsClientStreamImmediateClose(): Promise<void> { + const [serverConn, clientConn] = await tlsPair(); + await Promise.all([ + closeWriteAndClose(serverConn), + immediateClose(clientConn), + ]); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsClientAndServerStreamImmediateClose(): Promise<void> { + 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, +): Promise<void> { + const tlsPort = getPort(); + const tlsListener = Deno.listenTls({ + hostname: "localhost", + port: tlsPort, + certFile: "cli/tests/tls/localhost.crt", + keyFile: "cli/tests/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, + certFile: "cli/tests/tls/RootCA.crt", + }), + ]); + + 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 tcpForwardingInterruptPromise1 = deferred<void>(); + const tcpForwardingPromise1 = forwardBytes( + tcpConn2, + tcpConn1, + cipherByteCount, + tcpForwardingInterruptPromise1, + ); + + const tcpForwardingInterruptPromise2 = deferred<void>(); + const tcpForwardingPromise2 = forwardBytes( + tcpConn1, + tcpConn2, + Infinity, + tcpForwardingInterruptPromise2, + ); + + 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([ + assertThrowsAsync( + () => sendBytes(tlsConn1, 0x01, 1), + expectedError, + ), + assertThrowsAsync( + () => receiveBytes(tlsConn1, 0x02, 1), + expectedError, + ), + ]); + + const tlsTrafficPromise2 = Promise.all([ + assertThrowsAsync( + () => sendBytes(tlsConn2, 0x02, 1), + Deno.errors.UnexpectedEof, + ), + assertThrowsAsync( + () => 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; + + tcpForwardingInterruptPromise2.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), + ]); + + tcpForwardingInterruptPromise1.resolve(); + await tcpForwardingPromise1; + + switch (failureMode) { + case "corruption": + await sendBytes(tcpConn1, 0xff, 1 << 14 /* 16 kB */); + await assertThrowsAsync( + () => receiveEof(tlsConn1), + Deno.errors.InvalidData, + ); + tcpForwardingInterruptPromise2.resolve(); + break; + case "shutdown": + // Receiving a TCP FIN packet without receiving a TLS CloseNotify + // alert is not the expected mode of operation, but it is not a + // problem either, so it should be treated as if the TLS session was + // gracefully closed. + await Promise.all([ + tcpConn1.closeWrite(), + await receiveEof(tlsConn1), + 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, + ): Promise<void> { + 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, + ): Promise<void> { + 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: Deferred<void>, + ): Promise<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]); + if (nread == null) break; // Either EOF or interrupted. + const nwritten = await sink.write(buf.subarray(0, nread)); + assertStrictEquals(nread, nwritten); + count -= nwritten; + } + } +} + +unitTest( + { perms: { read: true, net: true } }, + async function tlsHandshakeWithTcpCorruptionImmediately() { + await tlsWithTcpFailureTestImpl("handshake", 0, "corruption", false); + await tlsWithTcpFailureTestImpl("handshake", 0, "corruption", true); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsHandshakeWithTcpShutdownImmediately() { + await tlsWithTcpFailureTestImpl("handshake", 0, "shutdown", false); + await tlsWithTcpFailureTestImpl("handshake", 0, "shutdown", true); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsHandshakeWithTcpCorruptionAfter70Bytes() { + await tlsWithTcpFailureTestImpl("handshake", 76, "corruption", false); + await tlsWithTcpFailureTestImpl("handshake", 78, "corruption", true); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsHandshakeWithTcpShutdownAfter70bytes() { + await tlsWithTcpFailureTestImpl("handshake", 77, "shutdown", false); + await tlsWithTcpFailureTestImpl("handshake", 79, "shutdown", true); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsHandshakeWithTcpCorruptionAfter200Bytes() { + await tlsWithTcpFailureTestImpl("handshake", 200, "corruption", false); + await tlsWithTcpFailureTestImpl("handshake", 202, "corruption", true); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsHandshakeWithTcpShutdownAfter200bytes() { + await tlsWithTcpFailureTestImpl("handshake", 201, "shutdown", false); + await tlsWithTcpFailureTestImpl("handshake", 203, "shutdown", true); + }, +); + +unitTest( + { perms: { read: true, net: true } }, + async function tlsTrafficWithTcpCorruption() { + await tlsWithTcpFailureTestImpl("traffic", Infinity, "corruption", false); + await tlsWithTcpFailureTestImpl("traffic", Infinity, "corruption", true); + }, +); + +unitTest( + { perms: { 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, + certFile: "./cli/tests/tls/localhost.crt", + keyFile: "./cli/tests/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 conn.write(resHead); + await conn.write(resBody); + + // Close TCP connection. + conn.close(); + } + } +} + +async function curl(url: string): Promise<string> { + const curl = Deno.run({ + cmd: ["curl", "--insecure", url], + stdout: "piped", + }); + + try { + const [status, output] = await Promise.all([curl.status(), curl.output()]); + if (!status.success) { + throw new Error(`curl ${url} failed: ${status.code}`); + } + return new TextDecoder().decode(output); + } finally { + curl.close(); + } +} + +unitTest( + { perms: { read: true, net: true, run: true } }, + async function curlFakeHttpsServer(): Promise<void> { + 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(); + }, +); + +unitTest( + { perms: { read: true, net: true } }, async function startTls(): Promise<void> { const hostname = "smtp.gmail.com"; const port = 587; |