Skip to content

Commit

Permalink
feat(ext/net): use rustls_tokio_stream (denoland#21205)
Browse files Browse the repository at this point in the history
Fixes denoland#21121 and denoland#19498

Migrates fully to rustls_tokio_stream. We no longer need to maintain our
own TlsStream implementation to properly support duplex.

This should fix a number of errors with TLS and websockets, HTTP and
"other" places where it's failing.
  • Loading branch information
mmastrac authored Nov 15, 2023
1 parent 4072672 commit 6b42cec
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 686 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ ring = "^0.17.0"
rusqlite = { version = "=0.29.0", features = ["unlock_notify", "bundled"] }
rustls = "0.21.8"
rustls-pemfile = "1.0.0"
rustls-tokio-stream = "=0.2.9"
rustls-tokio-stream = "=0.2.16"
rustls-webpki = "0.101.4"
webpki-roots = "0.25.2"
scopeguard = "1.2.0"
Expand Down
15 changes: 6 additions & 9 deletions cli/tests/integration/cert_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,12 @@ async fn listen_tls_alpn() {
let tcp_stream = tokio::net::TcpStream::connect("localhost:4504")
.await
.unwrap();
let mut tls_stream = TlsStream::new_client_side(tcp_stream, cfg, hostname);
let mut tls_stream =
TlsStream::new_client_side(tcp_stream, cfg, hostname, None);

tls_stream.handshake().await.unwrap();
let handshake = tls_stream.handshake().await.unwrap();

let (_, rustls_connection) = tls_stream.get_ref();
let alpn = rustls_connection.alpn_protocol().unwrap();
assert_eq!(alpn, b"foobar");
assert_eq!(handshake.alpn, Some(b"foobar".to_vec()));

let status = child.wait().unwrap();
assert!(status.success());
Expand Down Expand Up @@ -300,13 +299,11 @@ async fn listen_tls_alpn_fail() {
let tcp_stream = tokio::net::TcpStream::connect("localhost:4505")
.await
.unwrap();
let mut tls_stream = TlsStream::new_client_side(tcp_stream, cfg, hostname);
let mut tls_stream =
TlsStream::new_client_side(tcp_stream, cfg, hostname, None);

tls_stream.handshake().await.unwrap_err();

let (_, rustls_connection) = tls_stream.get_ref();
assert!(rustls_connection.alpn_protocol().is_none());

let status = child.wait().unwrap();
assert!(status.success());
}
4 changes: 1 addition & 3 deletions cli/tests/testdata/run/websocket_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,9 @@ Deno.test("websocket error", async () => {
ws.onopen = () => fail();
ws.onerror = (err) => {
assert(err instanceof ErrorEvent);

// Error message got changed because we don't use warp in test_util
assertEquals(
err.message,
"NetworkError: failed to connect to WebSocket: invalid data",
"NetworkError: failed to connect to WebSocket: received corrupt message of type InvalidContentType",
);
promise1.resolve();
};
Expand Down
66 changes: 45 additions & 21 deletions cli/tests/unit/tls_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} 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();
Expand Down Expand Up @@ -538,15 +539,23 @@ Deno.test(
},
);

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(1 << 20 /* 1 MB */);
const writeBuf = new Uint8Array(largeAmount);
writeBuf.fill(42);
await conn.write(writeBuf);
await writeAll(conn, writeBuf);

clearTimeout(timeout);

// Send EOF.
await conn.closeWrite();
Expand All @@ -564,14 +573,29 @@ async function sendAlotReceiveNothing(conn: Deno.Conn) {
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.
for (let nread = 0; nread < 1 << 20 /* 1 MB */; nread += n!) {
n = await conn.read(readBuf);
assertStrictEquals(typeof n, "number");
assert(n! > 0);
assertStrictEquals(readBuf[0], 42);
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();
Expand Down Expand Up @@ -623,7 +647,7 @@ async function sendReceiveEmptyBuf(conn: Deno.Conn) {

await assertRejects(async () => {
await conn.write(byteBuf);
}, Deno.errors.BrokenPipe);
}, Deno.errors.NotConnected);

n = await conn.write(emptyBuf);
assertStrictEquals(n, 0);
Expand Down Expand Up @@ -841,13 +865,12 @@ async function tlsWithTcpFailureTestImpl(
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 assertRejects(
() => receiveEof(tlsConn1),
Deno.errors.UnexpectedEof,
),
await tlsConn1.closeWrite(),
await receiveEof(tlsConn2),
]);
Expand Down Expand Up @@ -1036,8 +1059,8 @@ function createHttpsListener(port: number): Deno.Listener {
);

// Send response.
await conn.write(resHead);
await conn.write(resBody);
await writeAll(conn, resHead);
await writeAll(conn, resBody);

// Close TCP connection.
conn.close();
Expand All @@ -1046,12 +1069,14 @@ function createHttpsListener(port: number): Deno.Listener {
}

async function curl(url: string): Promise<string> {
const { success, code, stdout } = await new Deno.Command("curl", {
const { success, code, stdout, stderr } = await new Deno.Command("curl", {
args: ["--insecure", url],
}).output();

if (!success) {
throw new Error(`curl ${url} failed: ${code}`);
throw new Error(
`curl ${url} failed: ${code}:\n${new TextDecoder().decode(stderr)}`,
);
}
return new TextDecoder().decode(stdout);
}
Expand Down Expand Up @@ -1276,8 +1301,7 @@ Deno.test(
// Begin sending a 10mb blob over the TLS connection.
const whole = new Uint8Array(10 << 20); // 10mb.
whole.fill(42);
const sendPromise = conn1.write(whole);

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);
Expand All @@ -1294,7 +1318,7 @@ Deno.test(

// Receive second half of large blob. Wait for the send promise and check it.
assertEquals(await readFull(conn2, half), half.length);
assertEquals(await sendPromise, whole.length);
await sendPromise;

await conn1.handshake();
await conn2.handshake();
Expand Down Expand Up @@ -1352,7 +1376,7 @@ Deno.test(
await assertRejects(
() => conn.handshake(),
Deno.errors.InvalidData,
"UnknownIssuer",
"invalid peer certificate: UnknownIssuer",
);
conn.close();
}
Expand Down
8 changes: 4 additions & 4 deletions ext/http/http_next.rs
Original file line number Diff line number Diff line change
Expand Up @@ -850,15 +850,15 @@ fn serve_https(
});
spawn(
async {
io.handshake().await?;
let handshake = io.handshake().await?;
// If the client specifically negotiates a protocol, we will use it. If not, we'll auto-detect
// based on the prefix bytes
let handshake = io.get_ref().1.alpn_protocol();
if handshake == Some(TLS_ALPN_HTTP_2) {
let handshake = handshake.alpn;
if Some(TLS_ALPN_HTTP_2) == handshake.as_deref() {
serve_http2_unconditional(io, svc, listen_cancel_handle)
.await
.map_err(|e| e.into())
} else if handshake == Some(TLS_ALPN_HTTP_11) {
} else if Some(TLS_ALPN_HTTP_11) == handshake.as_deref() {
serve_http11_unconditional(io, svc, listen_cancel_handle)
.await
.map_err(|e| e.into())
Expand Down
1 change: 1 addition & 0 deletions ext/net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ deno_tls.workspace = true
enum-as-inner = "=0.5.1"
log.workspace = true
pin-project.workspace = true
rustls-tokio-stream.workspace = true
serde.workspace = true
socket2.workspace = true
tokio.workspace = true
Expand Down
Loading

0 comments on commit 6b42cec

Please sign in to comment.