Skip to content

Commit

Permalink
feat(ext/websocket): websockets over http2 (denoland#21040)
Browse files Browse the repository at this point in the history
Implements `WebSocket` over http/2. This requires a conformant http/2
server supporting the extended connect protocol.

Passes approximately 100 new WPT tests (mostly `?wpt_flags=h2` versions
of existing websockets APIs).

This is implemented as a fallback when http/1.1 fails, so a server that
supports both h1 and h2 WebSockets will still end up on the http/1.1
upgrade path.

The patch also cleas up the websockets handshake to split it up into
http, https+http1 and https+http2, making it a little less intertwined.

This uncovered a likely bug in the WPT test server:
web-platform-tests/wpt#42896
  • Loading branch information
mmastrac committed Nov 1, 2023
1 parent 587f2e0 commit 42c426e
Show file tree
Hide file tree
Showing 12 changed files with 625 additions and 230 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.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ futures = "0.3.21"
glob = "0.3.1"
hex = "0.4"
http = "0.2.9"
h2 = "0.3.17"
h2 = { version = "0.3.17", features = ["unstable"] }
httparse = "1.8.0"
hyper = { version = "0.14.26", features = ["runtime", "http1"] }
# TODO(mmastrac): indexmap 2.0 will require multiple synchronized changes
Expand Down 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.6"
rustls-tokio-stream = "=0.2.7"
rustls-webpki = "0.101.4"
rustls-native-certs = "0.6.2"
webpki-roots = "0.25.2"
Expand Down
2 changes: 1 addition & 1 deletion cli/tests/testdata/run/websocket_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Deno.test("websocket error", async () => {
// Error message got changed because we don't use warp in test_util
assertEquals(
err.message,
"InvalidData: invalid data",
"NetworkError: failed to connect to WebSocket: invalid data",
);
promise1.resolve();
};
Expand Down
54 changes: 50 additions & 4 deletions cli/tests/unit/websocket_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,50 @@ Deno.test(async function websocketConstructorTakeURLObjectAsParameter() {
await promise;
});

Deno.test(async function websocketH2SendSmallPacket() {
const promise = deferred();
const ws = new WebSocket(new URL("wss:https://localhost:4248/"));
assertEquals(ws.url, "wss:https://localhost:4248/");
let messageCount = 0;
ws.onerror = (e) => promise.reject(e);
ws.onopen = () => {
ws.send("a".repeat(16));
ws.send("a".repeat(16));
ws.send("a".repeat(16));
};
ws.onmessage = () => {
if (++messageCount == 3) {
ws.close();
}
};
ws.onclose = () => {
promise.resolve();
};
await promise;
});

Deno.test(async function websocketH2SendLargePacket() {
const promise = deferred();
const ws = new WebSocket(new URL("wss:https://localhost:4248/"));
assertEquals(ws.url, "wss:https://localhost:4248/");
let messageCount = 0;
ws.onerror = (e) => promise.reject(e);
ws.onopen = () => {
ws.send("a".repeat(65000));
ws.send("a".repeat(65000));
ws.send("a".repeat(65000));
};
ws.onmessage = () => {
if (++messageCount == 3) {
ws.close();
}
};
ws.onclose = () => {
promise.resolve();
};
await promise;
});

Deno.test(async function websocketSendLargePacket() {
const promise = deferred();
const ws = new WebSocket(new URL("wss:https://localhost:4243/"));
Expand All @@ -49,13 +93,14 @@ Deno.test(async function websocketSendLargePacket() {
Deno.test(async function websocketSendLargeBinaryPacket() {
const promise = deferred();
const ws = new WebSocket(new URL("wss:https://localhost:4243/"));
ws.binaryType = "arraybuffer";
assertEquals(ws.url, "wss:https://localhost:4243/");
ws.onerror = (e) => promise.reject(e);
ws.onopen = () => {
ws.send(new Uint8Array(65000));
};
ws.onmessage = (msg) => {
console.log(msg);
ws.onmessage = (msg: MessageEvent) => {
assertEquals(msg.data.byteLength, 65000);
ws.close();
};
ws.onclose = () => {
Expand All @@ -67,13 +112,14 @@ Deno.test(async function websocketSendLargeBinaryPacket() {
Deno.test(async function websocketSendLargeBlobPacket() {
const promise = deferred();
const ws = new WebSocket(new URL("wss:https://localhost:4243/"));
ws.binaryType = "arraybuffer";
assertEquals(ws.url, "wss:https://localhost:4243/");
ws.onerror = (e) => promise.reject(e);
ws.onopen = () => {
ws.send(new Blob(["a".repeat(65000)]));
};
ws.onmessage = (msg) => {
console.log(msg);
ws.onmessage = (msg: MessageEvent) => {
assertEquals(msg.data.byteLength, 65000);
ws.close();
};
ws.onclose = () => {
Expand Down
1 change: 1 addition & 0 deletions ext/fetch/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,7 @@ pub fn create_http_client(
options.ca_certs,
options.unsafely_ignore_certificate_errors,
options.client_cert_chain_and_key,
deno_tls::SocketUse::Http,
)?;

let mut alpn_protocols = vec![];
Expand Down
3 changes: 3 additions & 0 deletions ext/net/ops_tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use deno_tls::rustls::PrivateKey;
use deno_tls::rustls::ServerConfig;
use deno_tls::rustls::ServerConnection;
use deno_tls::rustls::ServerName;
use deno_tls::SocketUse;
use io::Error;
use io::Read;
use io::Write;
Expand Down Expand Up @@ -839,6 +840,7 @@ where
ca_certs,
unsafely_ignore_certificate_errors,
None,
SocketUse::GeneralSsl,
)?;

if let Some(alpn_protocols) = args.alpn_protocols {
Expand Down Expand Up @@ -938,6 +940,7 @@ where
ca_certs,
unsafely_ignore_certificate_errors,
cert_chain_and_key,
SocketUse::GeneralSsl,
)?;

if let Some(alpn_protocols) = args.alpn_protocols {
Expand Down
47 changes: 38 additions & 9 deletions ext/tls/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,23 @@ pub fn create_default_root_cert_store() -> RootCertStore {
root_cert_store
}

pub enum SocketUse {
/// General SSL: No ALPN
GeneralSsl,
/// HTTP: h1 and h2
Http,
/// http/1.1 only
Http1Only,
/// http/2 only
Http2Only,
}

pub fn create_client_config(
root_cert_store: Option<RootCertStore>,
ca_certs: Vec<Vec<u8>>,
unsafely_ignore_certificate_errors: Option<Vec<String>>,
client_cert_chain_and_key: Option<(String, String)>,
socket_use: SocketUse,
) -> Result<ClientConfig, AnyError> {
let maybe_cert_chain_and_key =
if let Some((cert_chain, private_key)) = client_cert_chain_and_key {
Expand All @@ -184,7 +196,7 @@ pub fn create_client_config(
// However it's not really feasible to deduplicate it as the `client_config` instances
// are not type-compatible - one wants "client cert", the other wants "transparency policy
// or client cert".
let client =
let mut client =
if let Some((cert_chain, private_key)) = maybe_cert_chain_and_key {
client_config
.with_client_auth_cert(cert_chain, private_key)
Expand All @@ -193,6 +205,7 @@ pub fn create_client_config(
client_config.with_no_client_auth()
};

add_alpn(&mut client, socket_use);
return Ok(client);
}

Expand Down Expand Up @@ -220,18 +233,34 @@ pub fn create_client_config(
root_cert_store
});

let client = if let Some((cert_chain, private_key)) = maybe_cert_chain_and_key
{
client_config
.with_client_auth_cert(cert_chain, private_key)
.expect("invalid client key or certificate")
} else {
client_config.with_no_client_auth()
};
let mut client =
if let Some((cert_chain, private_key)) = maybe_cert_chain_and_key {
client_config
.with_client_auth_cert(cert_chain, private_key)
.expect("invalid client key or certificate")
} else {
client_config.with_no_client_auth()
};

add_alpn(&mut client, socket_use);
Ok(client)
}

fn add_alpn(client: &mut ClientConfig, socket_use: SocketUse) {
match socket_use {
SocketUse::Http1Only => {
client.alpn_protocols = vec!["http/1.1".into()];
}
SocketUse::Http2Only => {
client.alpn_protocols = vec!["h2".into()];
}
SocketUse::Http => {
client.alpn_protocols = vec!["h2".into(), "http/1.1".into()];
}
SocketUse::GeneralSsl => {}
};
}

pub fn load_certs(
reader: &mut dyn BufRead,
) -> Result<Vec<Certificate>, AnyError> {
Expand Down
1 change: 1 addition & 0 deletions ext/websocket/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ deno_core.workspace = true
deno_net.workspace = true
deno_tls.workspace = true
fastwebsockets = { workspace = true, features = ["upgrade", "unstable-split"] }
h2.workspace = true
http.workspace = true
hyper = { workspace = true, features = ["backports"] }
once_cell.workspace = true
Expand Down
Loading

0 comments on commit 42c426e

Please sign in to comment.