diff --git a/Cargo.lock b/Cargo.lock index f35664536d..06f4415f2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1749,6 +1749,7 @@ dependencies = [ "deno_net", "deno_tls", "fastwebsockets", + "h2", "http", "hyper 0.14.27", "once_cell", @@ -4662,9 +4663,9 @@ dependencies = [ [[package]] name = "rustls-tokio-stream" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb55523a1a023f0e9eb66038515964b59dd0ef63f188ca759360a0e4cf0ee632" +checksum = "3abd2fa2e122bbf891a7333bf2091d8130367d8c381913821b24389208a3db45" dependencies = [ "futures", "rustls", diff --git a/Cargo.toml b/Cargo.toml index a0a6f56ddc..c57ba7fc67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 @@ -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" diff --git a/cli/tests/testdata/run/websocket_test.ts b/cli/tests/testdata/run/websocket_test.ts index 43db15ccee..b759d042d1 100644 --- a/cli/tests/testdata/run/websocket_test.ts +++ b/cli/tests/testdata/run/websocket_test.ts @@ -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(); }; diff --git a/cli/tests/unit/websocket_test.ts b/cli/tests/unit/websocket_test.ts index 8ae729d42b..697f524d08 100644 --- a/cli/tests/unit/websocket_test.ts +++ b/cli/tests/unit/websocket_test.ts @@ -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://localhost:4248/")); + assertEquals(ws.url, "wss://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://localhost:4248/")); + assertEquals(ws.url, "wss://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://localhost:4243/")); @@ -49,13 +93,14 @@ Deno.test(async function websocketSendLargePacket() { Deno.test(async function websocketSendLargeBinaryPacket() { const promise = deferred(); const ws = new WebSocket(new URL("wss://localhost:4243/")); + ws.binaryType = "arraybuffer"; assertEquals(ws.url, "wss://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 = () => { @@ -67,13 +112,14 @@ Deno.test(async function websocketSendLargeBinaryPacket() { Deno.test(async function websocketSendLargeBlobPacket() { const promise = deferred(); const ws = new WebSocket(new URL("wss://localhost:4243/")); + ws.binaryType = "arraybuffer"; assertEquals(ws.url, "wss://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 = () => { diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 797c5e2cd5..7cde5584f6 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -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![]; diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index fe0e70a5cf..26ec48fba2 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -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; @@ -839,6 +840,7 @@ where ca_certs, unsafely_ignore_certificate_errors, None, + SocketUse::GeneralSsl, )?; if let Some(alpn_protocols) = args.alpn_protocols { @@ -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 { diff --git a/ext/tls/lib.rs b/ext/tls/lib.rs index 3239466016..3f276fcd02 100644 --- a/ext/tls/lib.rs +++ b/ext/tls/lib.rs @@ -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, ca_certs: Vec>, unsafely_ignore_certificate_errors: Option>, client_cert_chain_and_key: Option<(String, String)>, + socket_use: SocketUse, ) -> Result { let maybe_cert_chain_and_key = if let Some((cert_chain, private_key)) = client_cert_chain_and_key { @@ -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) @@ -193,6 +205,7 @@ pub fn create_client_config( client_config.with_no_client_auth() }; + add_alpn(&mut client, socket_use); return Ok(client); } @@ -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, AnyError> { diff --git a/ext/websocket/Cargo.toml b/ext/websocket/Cargo.toml index da29203c49..a643e25a04 100644 --- a/ext/websocket/Cargo.toml +++ b/ext/websocket/Cargo.toml @@ -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 diff --git a/ext/websocket/lib.rs b/ext/websocket/lib.rs index ac40b8304c..c2599f6f61 100644 --- a/ext/websocket/lib.rs +++ b/ext/websocket/lib.rs @@ -1,16 +1,19 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::stream::WebSocketStream; use bytes::Bytes; +use deno_core::anyhow::bail; use deno_core::error::invalid_hostname; use deno_core::error::type_error; use deno_core::error::AnyError; +use deno_core::futures::TryFutureExt; use deno_core::op2; +use deno_core::unsync::spawn; use deno_core::url; use deno_core::AsyncMutFuture; use deno_core::AsyncRefCell; use deno_core::ByteString; -use deno_core::CancelFuture; use deno_core::CancelHandle; +use deno_core::CancelTryFuture; use deno_core::JsBuffer; use deno_core::OpState; use deno_core::RcRef; @@ -19,13 +22,16 @@ use deno_core::ResourceId; use deno_core::ToJsBuffer; use deno_net::raw::NetworkStream; use deno_tls::create_client_config; +use deno_tls::rustls::ClientConfig; use deno_tls::RootCertStoreProvider; +use deno_tls::SocketUse; use http::header::CONNECTION; use http::header::UPGRADE; use http::HeaderName; use http::HeaderValue; use http::Method; use http::Request; +use http::StatusCode; use http::Uri; use hyper::Body; use once_cell::sync::Lazy; @@ -146,31 +152,231 @@ pub struct CreateResponse { extensions: String, } -async fn handshake( - cancel_resource: Option>, +async fn handshake_websocket( + state: &Rc>, + uri: &Uri, + protocols: &str, + headers: Option>, +) -> Result<(WebSocket, http::HeaderMap), AnyError> { + let mut request = Request::builder().method(Method::GET).uri( + uri + .path_and_query() + .ok_or(type_error("Missing path in url".to_string()))? + .as_str(), + ); + + let authority = uri.authority().unwrap().as_str(); + let host = authority + .find('@') + .map(|idx| authority.split_at(idx + 1).1) + .unwrap_or_else(|| authority); + request = request + .header("Host", host) + .header(UPGRADE, "websocket") + .header(CONNECTION, "Upgrade") + .header( + "Sec-WebSocket-Key", + fastwebsockets::handshake::generate_key(), + ); + + let user_agent = state.borrow().borrow::().0.clone(); + request = + populate_common_request_headers(request, &user_agent, protocols, &headers)?; + + let request = request.body(Body::empty())?; + let domain = &uri.host().unwrap().to_string(); + let port = &uri.port_u16().unwrap_or(match uri.scheme_str() { + Some("wss") => 443, + Some("ws") => 80, + _ => unreachable!(), + }); + let addr = format!("{domain}:{port}"); + + let res = match uri.scheme_str() { + Some("ws") => handshake_http1_ws(request, &addr).await?, + Some("wss") => { + match handshake_http1_wss(state, request, domain, &addr).await { + Ok(res) => res, + Err(_) => { + handshake_http2_wss( + state, + uri, + authority, + &user_agent, + protocols, + domain, + &headers, + &addr, + ) + .await? + } + } + } + _ => unreachable!(), + }; + Ok(res) +} + +async fn handshake_http1_ws( + request: Request, + addr: &String, +) -> Result<(WebSocket, http::HeaderMap), AnyError> { + let tcp_socket = TcpStream::connect(addr).await?; + handshake_connection(request, tcp_socket).await +} + +async fn handshake_http1_wss( + state: &Rc>, + request: Request, + domain: &str, + addr: &str, +) -> Result<(WebSocket, http::HeaderMap), AnyError> { + let tcp_socket = TcpStream::connect(addr).await?; + let tls_config = create_ws_client_config(state, SocketUse::Http1Only)?; + let dnsname = + ServerName::try_from(domain).map_err(|_| invalid_hostname(domain))?; + let mut tls_connector = TlsStream::new_client_side( + tcp_socket, + tls_config.into(), + dnsname, + NonZeroUsize::new(65536), + ); + // If we can bail on an http/1.1 ALPN mismatch here, we can avoid doing extra work + tls_connector.handshake().await?; + handshake_connection(request, tls_connector).await +} + +#[allow(clippy::too_many_arguments)] +async fn handshake_http2_wss( + state: &Rc>, + uri: &Uri, + authority: &str, + user_agent: &str, + protocols: &str, + domain: &str, + headers: &Option>, + addr: &str, +) -> Result<(WebSocket, http::HeaderMap), AnyError> { + let tcp_socket = TcpStream::connect(addr).await?; + let tls_config = create_ws_client_config(state, SocketUse::Http2Only)?; + let dnsname = + ServerName::try_from(domain).map_err(|_| invalid_hostname(domain))?; + // We need to better expose the underlying errors here + let mut tls_connector = + TlsStream::new_client_side(tcp_socket, tls_config.into(), dnsname, None); + let handshake = tls_connector.handshake().await?; + if handshake.alpn.is_none() { + bail!("Didn't receive h2 alpn, aborting connection"); + } + let h2 = h2::client::Builder::new(); + let (mut send, conn) = h2.handshake::<_, Bytes>(tls_connector).await?; + spawn(conn); + let mut request = Request::builder(); + request = request.method(Method::CONNECT); + let uri = Uri::builder() + .authority(authority) + .path_and_query(uri.path_and_query().unwrap().as_str()) + .scheme("https") + .build()?; + request = request.uri(uri); + request = + populate_common_request_headers(request, user_agent, protocols, headers)?; + request = request.extension(h2::ext::Protocol::from("websocket")); + let (resp, send) = send.send_request(request.body(())?, false)?; + let resp = resp.await?; + if resp.status() != StatusCode::OK { + bail!("Invalid status code: {}", resp.status()); + } + let (http::response::Parts { headers, .. }, recv) = resp.into_parts(); + let mut stream = WebSocket::after_handshake( + WebSocketStream::new(stream::WsStreamKind::H2(send, recv), None), + Role::Client, + ); + // We currently don't support vectored writes in the H2 streams + stream.set_writev(false); + // TODO(mmastrac): we should be able to use a zero masking key over HTTPS + // stream.set_auto_apply_mask(false); + Ok((stream, headers)) +} + +async fn handshake_connection< + S: AsyncRead + AsyncWrite + Send + Unpin + 'static, +>( request: Request, socket: S, -) -> Result<(WebSocket, http::Response), AnyError> { - let client = - fastwebsockets::handshake::client(&LocalExecutor, request, socket); - - let (upgraded, response) = if let Some(cancel_resource) = cancel_resource { - client.or_cancel(cancel_resource).await? - } else { - client.await - } - .map_err(|err| { - DomExceptionNetworkError::new(&format!( - "failed to connect to WebSocket: {err}" - )) - })?; +) -> Result<(WebSocket, http::HeaderMap), AnyError> { + let (upgraded, response) = + fastwebsockets::handshake::client(&LocalExecutor, request, socket).await?; let upgraded = upgraded.into_inner(); let stream = WebSocketStream::new(stream::WsStreamKind::Upgraded(upgraded), None); let stream = WebSocket::after_handshake(stream, Role::Client); - Ok((stream, response)) + Ok((stream, response.into_parts().0.headers)) +} + +pub fn create_ws_client_config( + state: &Rc>, + socket_use: SocketUse, +) -> Result { + let unsafely_ignore_certificate_errors: Option> = state + .borrow() + .try_borrow::() + .and_then(|it| it.0.clone()); + let root_cert_store = state + .borrow() + .borrow::() + .get_or_try_init()?; + + create_client_config( + root_cert_store, + vec![], + unsafely_ignore_certificate_errors, + None, + socket_use, + ) +} + +/// Headers common to both http/1.1 and h2 requests. +fn populate_common_request_headers( + mut request: http::request::Builder, + user_agent: &str, + protocols: &str, + headers: &Option>, +) -> Result { + request = request + .header("User-Agent", user_agent) + .header("Sec-WebSocket-Version", "13"); + + if !protocols.is_empty() { + request = request.header("Sec-WebSocket-Protocol", protocols); + } + + if let Some(headers) = headers { + for (key, value) in headers { + let name = HeaderName::from_bytes(key) + .map_err(|err| type_error(err.to_string()))?; + let v = HeaderValue::from_bytes(value) + .map_err(|err| type_error(err.to_string()))?; + + let is_disallowed_header = matches!( + name, + http::header::HOST + | http::header::SEC_WEBSOCKET_ACCEPT + | http::header::SEC_WEBSOCKET_EXTENSIONS + | http::header::SEC_WEBSOCKET_KEY + | http::header::SEC_WEBSOCKET_PROTOCOL + | http::header::SEC_WEBSOCKET_VERSION + | http::header::UPGRADE + | http::header::CONNECTION + ); + if !is_disallowed_header { + request = request.header(name, v); + } + } + } + Ok(request) } #[op2(async)] @@ -205,99 +411,18 @@ where None }; - let unsafely_ignore_certificate_errors = state - .borrow() - .try_borrow::() - .and_then(|it| it.0.clone()); - let root_cert_store = state - .borrow() - .borrow::() - .get_or_try_init()?; - let user_agent = state.borrow().borrow::().0.clone(); let uri: Uri = url.parse()?; - let mut request = Request::builder().method(Method::GET).uri( - uri - .path_and_query() - .ok_or(type_error("Missing path in url".to_string()))? - .as_str(), - ); - let authority = uri.authority().unwrap().as_str(); - let host = authority - .find('@') - .map(|idx| authority.split_at(idx + 1).1) - .unwrap_or_else(|| authority); - request = request - .header("User-Agent", user_agent) - .header("Host", host) - .header(UPGRADE, "websocket") - .header(CONNECTION, "Upgrade") - .header( - "Sec-WebSocket-Key", - fastwebsockets::handshake::generate_key(), - ) - .header("Sec-WebSocket-Version", "13"); - - if !protocols.is_empty() { - request = request.header("Sec-WebSocket-Protocol", protocols); - } - - if let Some(headers) = headers { - for (key, value) in headers { - let name = HeaderName::from_bytes(&key) - .map_err(|err| type_error(err.to_string()))?; - let v = HeaderValue::from_bytes(&value) - .map_err(|err| type_error(err.to_string()))?; - - let is_disallowed_header = matches!( - name, - http::header::HOST - | http::header::SEC_WEBSOCKET_ACCEPT - | http::header::SEC_WEBSOCKET_EXTENSIONS - | http::header::SEC_WEBSOCKET_KEY - | http::header::SEC_WEBSOCKET_PROTOCOL - | http::header::SEC_WEBSOCKET_VERSION - | http::header::UPGRADE - | http::header::CONNECTION - ); - if !is_disallowed_header { - request = request.header(name, v); - } - } - } - - let request = request.body(Body::empty())?; - let domain = &uri.host().unwrap().to_string(); - let port = &uri.port_u16().unwrap_or(match uri.scheme_str() { - Some("wss") => 443, - Some("ws") => 80, - _ => unreachable!(), - }); - let addr = format!("{domain}:{port}"); - let tcp_socket = TcpStream::connect(addr).await?; - - let (stream, response) = match uri.scheme_str() { - Some("ws") => handshake(cancel_resource, request, tcp_socket).await?, - Some("wss") => { - let tls_config = create_client_config( - root_cert_store, - vec![], - unsafely_ignore_certificate_errors, - None, - )?; - let dnsname = ServerName::try_from(domain.as_str()) - .map_err(|_| invalid_hostname(domain))?; - let mut tls_connector = TlsStream::new_client_side( - tcp_socket, - tls_config.into(), - dnsname, - NonZeroUsize::new(65536), - ); - let _hs = tls_connector.handshake().await?; - handshake(cancel_resource, request, tls_connector).await? - } - _ => unreachable!(), - }; + let handshake = handshake_websocket(&state, &uri, &protocols, headers) + .map_err(|err| { + AnyError::from(DomExceptionNetworkError::new(&format!( + "failed to connect to WebSocket: {err}" + ))) + }); + let (stream, response) = match cancel_resource { + Some(rc) => handshake.try_or_cancel(rc).await, + None => handshake.await, + }?; if let Some(cancel_rid) = cancel_handle { if let Ok(res) = state.borrow_mut().resource_table.take_any(cancel_rid) { @@ -308,12 +433,11 @@ where let mut state = state.borrow_mut(); let rid = state.resource_table.add(ServerWebSocket::new(stream)); - let protocol = match response.headers().get("Sec-WebSocket-Protocol") { + let protocol = match response.get("Sec-WebSocket-Protocol") { Some(header) => header.to_str().unwrap(), None => "", }; let extensions = response - .headers() .get_all("Sec-WebSocket-Extensions") .iter() .map(|header| header.to_str().unwrap()) diff --git a/ext/websocket/stream.rs b/ext/websocket/stream.rs index 6f93406f62..7e36c81475 100644 --- a/ext/websocket/stream.rs +++ b/ext/websocket/stream.rs @@ -2,8 +2,12 @@ use bytes::Buf; use bytes::Bytes; use deno_net::raw::NetworkStream; +use h2::RecvStream; +use h2::SendStream; use hyper::upgrade::Upgraded; +use std::io::ErrorKind; use std::pin::Pin; +use std::task::ready; use std::task::Poll; use tokio::io::AsyncRead; use tokio::io::AsyncWrite; @@ -13,6 +17,7 @@ use tokio::io::ReadBuf; pub(crate) enum WsStreamKind { Upgraded(Upgraded), Network(NetworkStream), + H2(SendStream, RecvStream), } pub(crate) struct WebSocketStream { @@ -54,6 +59,27 @@ impl AsyncRead for WebSocketStream { match &mut self.stream { WsStreamKind::Network(stream) => Pin::new(stream).poll_read(cx, buf), WsStreamKind::Upgraded(stream) => Pin::new(stream).poll_read(cx, buf), + WsStreamKind::H2(_, recv) => { + let data = ready!(recv.poll_data(cx)); + let Some(data) = data else { + // EOF + return Poll::Ready(Ok(())); + }; + let mut data = data.map_err(|e| { + std::io::Error::new(std::io::ErrorKind::InvalidData, e) + })?; + recv.flow_control().release_capacity(data.len()).unwrap(); + // This looks like the prefix code above -- can we share this? + let copy_len = std::cmp::min(data.len(), buf.remaining()); + // TODO: There should be a way to do following two lines cleaner... + buf.put_slice(&data[..copy_len]); + data.advance(copy_len); + // Put back what's left + if !data.is_empty() { + self.pre = Some(data); + } + Poll::Ready(Ok(())) + } } } } @@ -67,6 +93,30 @@ impl AsyncWrite for WebSocketStream { match &mut self.stream { WsStreamKind::Network(stream) => Pin::new(stream).poll_write(cx, buf), WsStreamKind::Upgraded(stream) => Pin::new(stream).poll_write(cx, buf), + WsStreamKind::H2(send, _) => { + // Zero-length write succeeds + if buf.is_empty() { + return Poll::Ready(Ok(0)); + } + + send.reserve_capacity(buf.len()); + let res = ready!(send.poll_capacity(cx)); + + // TODO(mmastrac): the documentation is not entirely clear what to do here, so we'll continue + _ = res; + + // We'll try to send whatever we have capacity for + let size = std::cmp::min(buf.len(), send.capacity()); + assert!(size > 0); + + let buf: Bytes = Bytes::copy_from_slice(&buf[0..size]); + let len = buf.len(); + // TODO(mmastrac): surface the h2 error? + let res = send + .send_data(buf, false) + .map_err(|_| std::io::Error::from(ErrorKind::Other)); + Poll::Ready(res.map(|_| len)) + } } } @@ -77,6 +127,7 @@ impl AsyncWrite for WebSocketStream { match &mut self.stream { WsStreamKind::Network(stream) => Pin::new(stream).poll_flush(cx), WsStreamKind::Upgraded(stream) => Pin::new(stream).poll_flush(cx), + WsStreamKind::H2(..) => Poll::Ready(Ok(())), } } @@ -87,6 +138,13 @@ impl AsyncWrite for WebSocketStream { match &mut self.stream { WsStreamKind::Network(stream) => Pin::new(stream).poll_shutdown(cx), WsStreamKind::Upgraded(stream) => Pin::new(stream).poll_shutdown(cx), + WsStreamKind::H2(send, _) => { + // TODO(mmastrac): surface the h2 error? + let res = send + .send_data(Bytes::new(), false) + .map_err(|_| std::io::Error::from(ErrorKind::Other)); + Poll::Ready(res) + } } } @@ -94,6 +152,7 @@ impl AsyncWrite for WebSocketStream { match &self.stream { WsStreamKind::Network(stream) => stream.is_write_vectored(), WsStreamKind::Upgraded(stream) => stream.is_write_vectored(), + WsStreamKind::H2(..) => false, } } @@ -109,6 +168,10 @@ impl AsyncWrite for WebSocketStream { WsStreamKind::Upgraded(stream) => { Pin::new(stream).poll_write_vectored(cx, bufs) } + WsStreamKind::H2(..) => { + // TODO(mmastrac): this is possibly just too difficult, but we'll never call it + unimplemented!() + } } } } diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index 692a6a08c9..d63186311f 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -4,16 +4,28 @@ use anyhow::anyhow; use base64::prelude::BASE64_STANDARD; use base64::Engine; +use bytes::Bytes; use denokv_proto::datapath::AtomicWrite; use denokv_proto::datapath::AtomicWriteOutput; use denokv_proto::datapath::AtomicWriteStatus; use denokv_proto::datapath::ReadRangeOutput; use denokv_proto::datapath::SnapshotRead; use denokv_proto::datapath::SnapshotReadOutput; +use fastwebsockets::FragmentCollector; +use fastwebsockets::Frame; +use fastwebsockets::OpCode; +use fastwebsockets::Role; +use fastwebsockets::WebSocket; +use futures::future::join3; +use futures::future::poll_fn; use futures::Future; use futures::FutureExt; use futures::Stream; use futures::StreamExt; +use h2::server::Handshake; +use h2::server::SendResponse; +use h2::Reason; +use h2::RecvStream; use hyper::header::HeaderValue; use hyper::http; use hyper::server::Server; @@ -21,6 +33,7 @@ use hyper::service::make_service_fn; use hyper::service::service_fn; use hyper::upgrade::Upgraded; use hyper::Body; +use hyper::Method; use hyper::Request; use hyper::Response; use hyper::StatusCode; @@ -58,6 +71,7 @@ use std::sync::MutexGuard; use std::task::Context; use std::task::Poll; use std::time::Duration; +use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use tokio::net::TcpStream; @@ -105,6 +119,7 @@ const H2_ONLY_PORT: u16 = 5549; const HTTPS_CLIENT_AUTH_PORT: u16 = 5552; const WS_PORT: u16 = 4242; const WSS_PORT: u16 = 4243; +const WSS2_PORT: u16 = 4248; const WS_CLOSE_PORT: u16 = 4244; const WS_PING_PORT: u16 = 4245; const H2_GRPC_PORT: u16 = 4246; @@ -399,9 +414,6 @@ async fn run_ws_server(addr: &SocketAddr) { async fn ping_websocket_handler( ws: fastwebsockets::WebSocket, ) -> Result<(), anyhow::Error> { - use fastwebsockets::Frame; - use fastwebsockets::OpCode; - let mut ws = fastwebsockets::FragmentCollector::new(ws); for i in 0..9 { @@ -458,6 +470,126 @@ async fn run_ws_close_server(addr: &SocketAddr) { } } +async fn handle_wss_stream( + recv: Request, + mut send: SendResponse, +) -> Result<(), h2::Error> { + if recv.method() != Method::CONNECT { + eprintln!("wss2: refusing non-CONNECT stream"); + send.send_reset(Reason::REFUSED_STREAM); + return Ok(()); + } + let Some(protocol) = recv.extensions().get::() else { + eprintln!("wss2: refusing no-:protocol stream"); + send.send_reset(Reason::REFUSED_STREAM); + return Ok(()); + }; + if protocol.as_str() != "websocket" && protocol.as_str() != "WebSocket" { + eprintln!("wss2: refusing non-websocket stream"); + send.send_reset(Reason::REFUSED_STREAM); + return Ok(()); + } + let mut body = recv.into_body(); + let mut response = Response::new(()); + *response.status_mut() = StatusCode::OK; + let mut resp = send.send_response(response, false)?; + // Use a duplex stream to talk to fastwebsockets because it's just faster to implement + let (a, b) = tokio::io::duplex(65536); + let f1 = tokio::spawn(tokio::task::unconstrained(async move { + let ws = WebSocket::after_handshake(a, Role::Server); + let mut ws = FragmentCollector::new(ws); + loop { + let frame = ws.read_frame().await.unwrap(); + if frame.opcode == OpCode::Close { + break; + } + ws.write_frame(frame).await.unwrap(); + } + })); + let (mut br, mut bw) = tokio::io::split(b); + let f2 = tokio::spawn(tokio::task::unconstrained(async move { + loop { + let Some(Ok(data)) = poll_fn(|cx| body.poll_data(cx)).await else { + return; + }; + body.flow_control().release_capacity(data.len()).unwrap(); + let Ok(_) = bw.write_all(&data).await else { + break; + }; + } + })); + let f3 = tokio::spawn(tokio::task::unconstrained(async move { + loop { + let mut buf = [0; 65536]; + let n = br.read(&mut buf).await.unwrap(); + if n == 0 { + break; + } + resp.reserve_capacity(n); + poll_fn(|cx| resp.poll_capacity(cx)).await; + resp + .send_data(Bytes::copy_from_slice(&buf[0..n]), false) + .unwrap(); + } + resp.send_data(Bytes::new(), true).unwrap(); + })); + _ = join3(f1, f2, f3).await; + Ok(()) +} + +async fn run_wss2_server(addr: &SocketAddr) { + let cert_file = "tls/localhost.crt"; + let key_file = "tls/localhost.key"; + let ca_cert_file = "tls/RootCA.pem"; + + let tls_config = get_tls_config( + cert_file, + key_file, + ca_cert_file, + SupportedHttpVersions::Http2Only, + ) + .await + .unwrap(); + let tls_acceptor = TlsAcceptor::from(tls_config); + + let listener = TcpListener::bind(addr).await.unwrap(); + while let Ok((stream, _addr)) = listener.accept().await { + match tls_acceptor.accept(stream).await { + Ok(tls) => { + tokio::spawn(async move { + let mut h2 = h2::server::Builder::new(); + h2.enable_connect_protocol(); + // Using Bytes is pretty alloc-heavy but this is a test server + let server: Handshake<_, Bytes> = h2.handshake(tls); + let mut server = match server.await { + Ok(server) => server, + Err(e) => { + println!("Failed to handshake h2: {e:?}"); + return; + } + }; + loop { + let Some(conn) = server.accept().await else { + break; + }; + let (recv, send) = match conn { + Ok(conn) => conn, + Err(e) => { + println!("Failed to accept a connection: {e:?}"); + break; + } + }; + tokio::spawn(handle_wss_stream(recv, send)); + } + }); + } + Err(e) => { + println!("Failed to accept TLS: {e:?}"); + } + } + } +} + #[derive(Default)] enum SupportedHttpVersions { #[default] @@ -1933,6 +2065,8 @@ pub async fn run_all_servers() { let wss_server_fut = run_wss_server(&wss_addr); let ws_close_addr = SocketAddr::from(([127, 0, 0, 1], WS_CLOSE_PORT)); let ws_close_server_fut = run_ws_close_server(&ws_close_addr); + let wss2_addr = SocketAddr::from(([127, 0, 0, 1], WSS2_PORT)); + let wss2_server_fut = run_wss2_server(&wss2_addr); let tls_server_fut = run_tls_server(); let tls_client_auth_server_fut = run_tls_client_auth_server(); @@ -1952,6 +2086,7 @@ pub async fn run_all_servers() { ws_server_fut, ws_ping_server_fut, wss_server_fut, + wss2_server_fut, tls_server_fut, tls_client_auth_server_fut, ws_close_server_fut, diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json index 9d016d2823..4b01f0c006 100644 --- a/tools/wpt/expectation.json +++ b/tools/wpt/expectation.json @@ -7744,106 +7744,106 @@ }, "websockets": { "Close-1000-reason.any.html?default": true, - "Close-1000-reason.any.html?wpt_flags=h2": false, + "Close-1000-reason.any.html?wpt_flags=h2": true, "Close-1000-reason.any.html?wss": true, "Close-1000-reason.any.worker.html?default": true, - "Close-1000-reason.any.worker.html?wpt_flags=h2": false, + "Close-1000-reason.any.worker.html?wpt_flags=h2": true, "Close-1000-reason.any.worker.html?wss": true, "Close-1000-verify-code.any.html?default": true, - "Close-1000-verify-code.any.html?wpt_flags=h2": false, + "Close-1000-verify-code.any.html?wpt_flags=h2": true, "Close-1000-verify-code.any.html?wss": true, "Close-1000-verify-code.any.worker.html?default": true, - "Close-1000-verify-code.any.worker.html?wpt_flags=h2": false, + "Close-1000-verify-code.any.worker.html?wpt_flags=h2": true, "Close-1000-verify-code.any.worker.html?wss": true, "Close-1000.any.html?default": true, - "Close-1000.any.html?wpt_flags=h2": false, + "Close-1000.any.html?wpt_flags=h2": true, "Close-1000.any.html?wss": true, "Close-1000.any.worker.html?default": true, - "Close-1000.any.worker.html?wpt_flags=h2": false, + "Close-1000.any.worker.html?wpt_flags=h2": true, "Close-1000.any.worker.html?wss": true, "Close-1005-verify-code.any.html?default": true, - "Close-1005-verify-code.any.html?wpt_flags=h2": false, + "Close-1005-verify-code.any.html?wpt_flags=h2": true, "Close-1005-verify-code.any.html?wss": true, "Close-1005-verify-code.any.worker.html?default": true, - "Close-1005-verify-code.any.worker.html?wpt_flags=h2": false, + "Close-1005-verify-code.any.worker.html?wpt_flags=h2": true, "Close-1005-verify-code.any.worker.html?wss": true, "Close-1005.any.html?default": true, - "Close-1005.any.html?wpt_flags=h2": false, + "Close-1005.any.html?wpt_flags=h2": true, "Close-1005.any.html?wss": true, "Close-1005.any.worker.html?default": true, - "Close-1005.any.worker.html?wpt_flags=h2": false, + "Close-1005.any.worker.html?wpt_flags=h2": true, "Close-1005.any.worker.html?wss": true, "Close-2999-reason.any.html?default": true, - "Close-2999-reason.any.html?wpt_flags=h2": false, + "Close-2999-reason.any.html?wpt_flags=h2": true, "Close-2999-reason.any.html?wss": true, "Close-2999-reason.any.worker.html?default": true, - "Close-2999-reason.any.worker.html?wpt_flags=h2": false, + "Close-2999-reason.any.worker.html?wpt_flags=h2": true, "Close-2999-reason.any.worker.html?wss": true, "Close-3000-reason.any.html?default": true, - "Close-3000-reason.any.html?wpt_flags=h2": false, + "Close-3000-reason.any.html?wpt_flags=h2": true, "Close-3000-reason.any.html?wss": true, "Close-3000-reason.any.worker.html?default": true, - "Close-3000-reason.any.worker.html?wpt_flags=h2": false, + "Close-3000-reason.any.worker.html?wpt_flags=h2": true, "Close-3000-reason.any.worker.html?wss": true, "Close-3000-verify-code.any.html?default": true, - "Close-3000-verify-code.any.html?wpt_flags=h2": false, + "Close-3000-verify-code.any.html?wpt_flags=h2": true, "Close-3000-verify-code.any.html?wss": true, "Close-3000-verify-code.any.worker.html?default": true, - "Close-3000-verify-code.any.worker.html?wpt_flags=h2": false, + "Close-3000-verify-code.any.worker.html?wpt_flags=h2": true, "Close-3000-verify-code.any.worker.html?wss": true, "Close-4999-reason.any.html?default": true, - "Close-4999-reason.any.html?wpt_flags=h2": false, + "Close-4999-reason.any.html?wpt_flags=h2": true, "Close-4999-reason.any.html?wss": true, "Close-4999-reason.any.worker.html?default": true, - "Close-4999-reason.any.worker.html?wpt_flags=h2": false, + "Close-4999-reason.any.worker.html?wpt_flags=h2": true, "Close-4999-reason.any.worker.html?wss": true, "Close-Reason-124Bytes.any.html?default": true, - "Close-Reason-124Bytes.any.html?wpt_flags=h2": false, + "Close-Reason-124Bytes.any.html?wpt_flags=h2": true, "Close-Reason-124Bytes.any.html?wss": true, "Close-Reason-124Bytes.any.worker.html?default": true, - "Close-Reason-124Bytes.any.worker.html?wpt_flags=h2": false, + "Close-Reason-124Bytes.any.worker.html?wpt_flags=h2": true, "Close-Reason-124Bytes.any.worker.html?wss": true, "Close-delayed.any.html?default": true, - "Close-delayed.any.html?wpt_flags=h2": false, + "Close-delayed.any.html?wpt_flags=h2": true, "Close-delayed.any.html?wss": true, "Close-delayed.any.worker.html?default": true, - "Close-delayed.any.worker.html?wpt_flags=h2": false, + "Close-delayed.any.worker.html?wpt_flags=h2": true, "Close-delayed.any.worker.html?wss": true, "Close-onlyReason.any.html?default": true, - "Close-onlyReason.any.html?wpt_flags=h2": false, + "Close-onlyReason.any.html?wpt_flags=h2": true, "Close-onlyReason.any.html?wss": true, "Close-onlyReason.any.worker.html?default": true, - "Close-onlyReason.any.worker.html?wpt_flags=h2": false, + "Close-onlyReason.any.worker.html?wpt_flags=h2": true, "Close-onlyReason.any.worker.html?wss": true, "Close-readyState-Closed.any.html?default": true, - "Close-readyState-Closed.any.html?wpt_flags=h2": false, + "Close-readyState-Closed.any.html?wpt_flags=h2": true, "Close-readyState-Closed.any.html?wss": true, "Close-readyState-Closed.any.worker.html?default": true, - "Close-readyState-Closed.any.worker.html?wpt_flags=h2": false, + "Close-readyState-Closed.any.worker.html?wpt_flags=h2": true, "Close-readyState-Closed.any.worker.html?wss": true, "Close-readyState-Closing.any.html?default": true, - "Close-readyState-Closing.any.html?wpt_flags=h2": false, + "Close-readyState-Closing.any.html?wpt_flags=h2": true, "Close-readyState-Closing.any.html?wss": true, "Close-readyState-Closing.any.worker.html?default": true, - "Close-readyState-Closing.any.worker.html?wpt_flags=h2": false, + "Close-readyState-Closing.any.worker.html?wpt_flags=h2": true, "Close-readyState-Closing.any.worker.html?wss": true, "Close-reason-unpaired-surrogates.any.html?default": true, - "Close-reason-unpaired-surrogates.any.html?wpt_flags=h2": false, + "Close-reason-unpaired-surrogates.any.html?wpt_flags=h2": true, "Close-reason-unpaired-surrogates.any.html?wss": true, "Close-reason-unpaired-surrogates.any.worker.html?default": true, - "Close-reason-unpaired-surrogates.any.worker.html?wpt_flags=h2": false, + "Close-reason-unpaired-surrogates.any.worker.html?wpt_flags=h2": true, "Close-reason-unpaired-surrogates.any.worker.html?wss": true, "Close-server-initiated-close.any.html?default": true, - "Close-server-initiated-close.any.html?wpt_flags=h2": false, + "Close-server-initiated-close.any.html?wpt_flags=h2": true, "Close-server-initiated-close.any.html?wss": true, "Close-server-initiated-close.any.worker.html?default": true, - "Close-server-initiated-close.any.worker.html?wpt_flags=h2": false, + "Close-server-initiated-close.any.worker.html?wpt_flags=h2": true, "Close-server-initiated-close.any.worker.html?wss": true, "Close-undefined.any.html?default": true, - "Close-undefined.any.html?wpt_flags=h2": false, + "Close-undefined.any.html?wpt_flags=h2": true, "Close-undefined.any.html?wss": true, "Close-undefined.any.worker.html?default": true, - "Close-undefined.any.worker.html?wpt_flags=h2": false, + "Close-undefined.any.worker.html?wpt_flags=h2": true, "Close-undefined.any.worker.html?wss": true, "Create-asciiSep-protocol-string.any.html?default": true, "Create-asciiSep-protocol-string.any.html?wpt_flags=h2": true, @@ -7852,20 +7852,16 @@ "Create-asciiSep-protocol-string.any.worker.html?wpt_flags=h2": true, "Create-asciiSep-protocol-string.any.worker.html?wss": true, "Create-blocked-port.any.html?default": true, - "Create-blocked-port.any.html?wpt_flags=h2": [ - "Basic check" - ], + "Create-blocked-port.any.html?wpt_flags=h2": true, "Create-blocked-port.any.html?wss": true, "Create-blocked-port.any.worker.html?default": true, - "Create-blocked-port.any.worker.html?wpt_flags=h2": [ - "Basic check" - ], + "Create-blocked-port.any.worker.html?wpt_flags=h2": true, "Create-blocked-port.any.worker.html?wss": true, "Create-extensions-empty.any.html?default": true, - "Create-extensions-empty.any.html?wpt_flags=h2": false, + "Create-extensions-empty.any.html?wpt_flags=h2": true, "Create-extensions-empty.any.html?wss": true, "Create-extensions-empty.any.worker.html?default": true, - "Create-extensions-empty.any.worker.html?wpt_flags=h2": false, + "Create-extensions-empty.any.worker.html?wpt_flags=h2": true, "Create-extensions-empty.any.worker.html?wss": true, "Create-http-urls.any.html": true, "Create-http-urls.any.worker.html": true, @@ -7906,16 +7902,16 @@ "Create-url-with-space.any.worker.html?wpt_flags=h2": true, "Create-url-with-space.any.worker.html?wss": true, "Create-valid-url-array-protocols.any.html?default": true, - "Create-valid-url-array-protocols.any.html?wpt_flags=h2": false, + "Create-valid-url-array-protocols.any.html?wpt_flags=h2": true, "Create-valid-url-array-protocols.any.html?wss": true, "Create-valid-url-array-protocols.any.worker.html?default": true, - "Create-valid-url-array-protocols.any.worker.html?wpt_flags=h2": false, + "Create-valid-url-array-protocols.any.worker.html?wpt_flags=h2": true, "Create-valid-url-array-protocols.any.worker.html?wss": true, "Create-valid-url-binaryType-blob.any.html?default": true, - "Create-valid-url-binaryType-blob.any.html?wpt_flags=h2": false, + "Create-valid-url-binaryType-blob.any.html?wpt_flags=h2": true, "Create-valid-url-binaryType-blob.any.html?wss": true, "Create-valid-url-binaryType-blob.any.worker.html?default": true, - "Create-valid-url-binaryType-blob.any.worker.html?wpt_flags=h2": false, + "Create-valid-url-binaryType-blob.any.worker.html?wpt_flags=h2": true, "Create-valid-url-binaryType-blob.any.worker.html?wss": true, "Create-valid-url-protocol-empty.any.html?default": true, "Create-valid-url-protocol-empty.any.html?wpt_flags=h2": true, @@ -7924,40 +7920,40 @@ "Create-valid-url-protocol-empty.any.worker.html?wpt_flags=h2": true, "Create-valid-url-protocol-empty.any.worker.html?wss": true, "Create-valid-url-protocol-setCorrectly.any.html?default": true, - "Create-valid-url-protocol-setCorrectly.any.html?wpt_flags=h2": false, + "Create-valid-url-protocol-setCorrectly.any.html?wpt_flags=h2": true, "Create-valid-url-protocol-setCorrectly.any.html?wss": true, "Create-valid-url-protocol-setCorrectly.any.worker.html?default": true, - "Create-valid-url-protocol-setCorrectly.any.worker.html?wpt_flags=h2": false, + "Create-valid-url-protocol-setCorrectly.any.worker.html?wpt_flags=h2": true, "Create-valid-url-protocol-setCorrectly.any.worker.html?wss": true, "Create-valid-url-protocol-string.any.html?default": true, - "Create-valid-url-protocol-string.any.html?wpt_flags=h2": false, + "Create-valid-url-protocol-string.any.html?wpt_flags=h2": true, "Create-valid-url-protocol-string.any.html?wss": true, "Create-valid-url-protocol-string.any.worker.html?default": true, - "Create-valid-url-protocol-string.any.worker.html?wpt_flags=h2": false, + "Create-valid-url-protocol-string.any.worker.html?wpt_flags=h2": true, "Create-valid-url-protocol-string.any.worker.html?wss": true, "Create-valid-url-protocol.any.html?default": true, - "Create-valid-url-protocol.any.html?wpt_flags=h2": false, + "Create-valid-url-protocol.any.html?wpt_flags=h2": true, "Create-valid-url-protocol.any.html?wss": true, "Create-valid-url-protocol.any.worker.html?default": true, - "Create-valid-url-protocol.any.worker.html?wpt_flags=h2": false, + "Create-valid-url-protocol.any.worker.html?wpt_flags=h2": true, "Create-valid-url-protocol.any.worker.html?wss": true, "Create-valid-url.any.html?default": true, - "Create-valid-url.any.html?wpt_flags=h2": false, + "Create-valid-url.any.html?wpt_flags=h2": true, "Create-valid-url.any.html?wss": true, "Create-valid-url.any.worker.html?default": true, - "Create-valid-url.any.worker.html?wpt_flags=h2": false, + "Create-valid-url.any.worker.html?wpt_flags=h2": true, "Create-valid-url.any.worker.html?wss": true, "Send-0byte-data.any.html?default": true, - "Send-0byte-data.any.html?wpt_flags=h2": false, + "Send-0byte-data.any.html?wpt_flags=h2": true, "Send-0byte-data.any.html?wss": true, "Send-0byte-data.any.worker.html?default": true, - "Send-0byte-data.any.worker.html?wpt_flags=h2": false, + "Send-0byte-data.any.worker.html?wpt_flags=h2": true, "Send-0byte-data.any.worker.html?wss": true, "Send-65K-data.any.html?default": true, - "Send-65K-data.any.html?wpt_flags=h2": false, + "Send-65K-data.any.html?wpt_flags=h2": true, "Send-65K-data.any.html?wss": true, "Send-65K-data.any.worker.html?default": true, - "Send-65K-data.any.worker.html?wpt_flags=h2": false, + "Send-65K-data.any.worker.html?wpt_flags=h2": true, "Send-65K-data.any.worker.html?wss": true, "Send-before-open.any.html?default": true, "Send-before-open.any.html?wpt_flags=h2": true, @@ -7966,109 +7962,109 @@ "Send-before-open.any.worker.html?wpt_flags=h2": true, "Send-before-open.any.worker.html?wss": true, "Send-binary-65K-arraybuffer.any.html?default": true, - "Send-binary-65K-arraybuffer.any.html?wpt_flags=h2": false, + "Send-binary-65K-arraybuffer.any.html?wpt_flags=h2": true, "Send-binary-65K-arraybuffer.any.html?wss": true, "Send-binary-65K-arraybuffer.any.worker.html?default": true, - "Send-binary-65K-arraybuffer.any.worker.html?wpt_flags=h2": false, + "Send-binary-65K-arraybuffer.any.worker.html?wpt_flags=h2": true, "Send-binary-65K-arraybuffer.any.worker.html?wss": true, "Send-binary-arraybuffer.any.html?default": true, - "Send-binary-arraybuffer.any.html?wpt_flags=h2": false, + "Send-binary-arraybuffer.any.html?wpt_flags=h2": true, "Send-binary-arraybuffer.any.html?wss": true, "Send-binary-arraybuffer.any.worker.html?default": true, - "Send-binary-arraybuffer.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybuffer.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybuffer.any.worker.html?wss": true, "Send-binary-arraybufferview-float32.any.html?default": true, - "Send-binary-arraybufferview-float32.any.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-float32.any.html?wpt_flags=h2": true, "Send-binary-arraybufferview-float32.any.html?wss": true, "Send-binary-arraybufferview-float32.any.worker.html?default": true, - "Send-binary-arraybufferview-float32.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-float32.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybufferview-float32.any.worker.html?wss": true, "Send-binary-arraybufferview-float64.any.html?default": true, - "Send-binary-arraybufferview-float64.any.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-float64.any.html?wpt_flags=h2": true, "Send-binary-arraybufferview-float64.any.html?wss": true, "Send-binary-arraybufferview-float64.any.worker.html?default": true, - "Send-binary-arraybufferview-float64.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-float64.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybufferview-float64.any.worker.html?wss": true, "Send-binary-arraybufferview-int16-offset.any.html?default": true, - "Send-binary-arraybufferview-int16-offset.any.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-int16-offset.any.html?wpt_flags=h2": true, "Send-binary-arraybufferview-int16-offset.any.html?wss": true, "Send-binary-arraybufferview-int16-offset.any.worker.html?default": true, - "Send-binary-arraybufferview-int16-offset.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-int16-offset.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybufferview-int16-offset.any.worker.html?wss": true, "Send-binary-arraybufferview-int32.any.html?default": true, - "Send-binary-arraybufferview-int32.any.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-int32.any.html?wpt_flags=h2": true, "Send-binary-arraybufferview-int32.any.html?wss": true, "Send-binary-arraybufferview-int32.any.worker.html?default": true, - "Send-binary-arraybufferview-int32.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-int32.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybufferview-int32.any.worker.html?wss": true, "Send-binary-arraybufferview-int8.any.html?default": true, - "Send-binary-arraybufferview-int8.any.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-int8.any.html?wpt_flags=h2": true, "Send-binary-arraybufferview-int8.any.html?wss": true, "Send-binary-arraybufferview-int8.any.worker.html?default": true, - "Send-binary-arraybufferview-int8.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-int8.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybufferview-int8.any.worker.html?wss": true, "Send-binary-arraybufferview-uint16-offset-length.any.html?default": true, - "Send-binary-arraybufferview-uint16-offset-length.any.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-uint16-offset-length.any.html?wpt_flags=h2": true, "Send-binary-arraybufferview-uint16-offset-length.any.html?wss": true, "Send-binary-arraybufferview-uint16-offset-length.any.worker.html?default": true, - "Send-binary-arraybufferview-uint16-offset-length.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-uint16-offset-length.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybufferview-uint16-offset-length.any.worker.html?wss": true, "Send-binary-arraybufferview-uint32-offset.any.html?default": true, - "Send-binary-arraybufferview-uint32-offset.any.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-uint32-offset.any.html?wpt_flags=h2": true, "Send-binary-arraybufferview-uint32-offset.any.html?wss": true, "Send-binary-arraybufferview-uint32-offset.any.worker.html?default": true, - "Send-binary-arraybufferview-uint32-offset.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-uint32-offset.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybufferview-uint32-offset.any.worker.html?wss": true, "Send-binary-arraybufferview-uint8-offset-length.any.html?default": true, - "Send-binary-arraybufferview-uint8-offset-length.any.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-uint8-offset-length.any.html?wpt_flags=h2": true, "Send-binary-arraybufferview-uint8-offset-length.any.html?wss": true, "Send-binary-arraybufferview-uint8-offset-length.any.worker.html?default": true, - "Send-binary-arraybufferview-uint8-offset-length.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-uint8-offset-length.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybufferview-uint8-offset-length.any.worker.html?wss": true, "Send-binary-arraybufferview-uint8-offset.any.html?default": true, - "Send-binary-arraybufferview-uint8-offset.any.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-uint8-offset.any.html?wpt_flags=h2": true, "Send-binary-arraybufferview-uint8-offset.any.html?wss": true, "Send-binary-arraybufferview-uint8-offset.any.worker.html?default": true, - "Send-binary-arraybufferview-uint8-offset.any.worker.html?wpt_flags=h2": false, + "Send-binary-arraybufferview-uint8-offset.any.worker.html?wpt_flags=h2": true, "Send-binary-arraybufferview-uint8-offset.any.worker.html?wss": true, "Send-binary-blob.any.html?default": true, - "Send-binary-blob.any.html?wpt_flags=h2": false, + "Send-binary-blob.any.html?wpt_flags=h2": true, "Send-binary-blob.any.html?wss": true, "Send-binary-blob.any.worker.html?default": true, - "Send-binary-blob.any.worker.html?wpt_flags=h2": false, + "Send-binary-blob.any.worker.html?wpt_flags=h2": true, "Send-binary-blob.any.worker.html?wss": true, "Send-data.any.html?default": true, - "Send-data.any.html?wpt_flags=h2": false, + "Send-data.any.html?wpt_flags=h2": true, "Send-data.any.html?wss": true, "Send-data.any.worker.html?default": true, - "Send-data.any.worker.html?wpt_flags=h2": false, + "Send-data.any.worker.html?wpt_flags=h2": true, "Send-data.any.worker.html?wss": true, "Send-data.worker.html?default": true, - "Send-data.worker.html?wpt_flags=h2": false, + "Send-data.worker.html?wpt_flags=h2": true, "Send-data.worker.html?wss": true, "Send-null.any.html?default": true, - "Send-null.any.html?wpt_flags=h2": false, + "Send-null.any.html?wpt_flags=h2": true, "Send-null.any.html?wss": true, "Send-null.any.worker.html?default": true, - "Send-null.any.worker.html?wpt_flags=h2": false, + "Send-null.any.worker.html?wpt_flags=h2": true, "Send-null.any.worker.html?wss": true, "Send-paired-surrogates.any.html?default": true, - "Send-paired-surrogates.any.html?wpt_flags=h2": false, + "Send-paired-surrogates.any.html?wpt_flags=h2": true, "Send-paired-surrogates.any.html?wss": true, "Send-paired-surrogates.any.worker.html?default": true, - "Send-paired-surrogates.any.worker.html?wpt_flags=h2": false, + "Send-paired-surrogates.any.worker.html?wpt_flags=h2": true, "Send-paired-surrogates.any.worker.html?wss": true, "Send-unicode-data.any.html?default": true, - "Send-unicode-data.any.html?wpt_flags=h2": false, + "Send-unicode-data.any.html?wpt_flags=h2": true, "Send-unicode-data.any.html?wss": true, "Send-unicode-data.any.worker.html?default": true, - "Send-unicode-data.any.worker.html?wpt_flags=h2": false, + "Send-unicode-data.any.worker.html?wpt_flags=h2": true, "Send-unicode-data.any.worker.html?wss": true, "Send-unpaired-surrogates.any.html?default": true, - "Send-unpaired-surrogates.any.html?wpt_flags=h2": false, + "Send-unpaired-surrogates.any.html?wpt_flags=h2": true, "Send-unpaired-surrogates.any.html?wss": true, "Send-unpaired-surrogates.any.worker.html?default": true, - "Send-unpaired-surrogates.any.worker.html?wpt_flags=h2": false, + "Send-unpaired-surrogates.any.worker.html?wpt_flags=h2": true, "Send-unpaired-surrogates.any.worker.html?wss": true, "back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.html": false, "back-forward-cache-with-closed-websocket-connection.window.html": false, @@ -8079,10 +8075,10 @@ "basic-auth.any.worker.html?wpt_flags=h2": false, "basic-auth.any.worker.html?wss": false, "binaryType-wrong-value.any.html?default": true, - "binaryType-wrong-value.any.html?wpt_flags=h2": false, + "binaryType-wrong-value.any.html?wpt_flags=h2": true, "binaryType-wrong-value.any.html?wss": true, "binaryType-wrong-value.any.worker.html?default": true, - "binaryType-wrong-value.any.worker.html?wpt_flags=h2": false, + "binaryType-wrong-value.any.worker.html?wpt_flags=h2": true, "binaryType-wrong-value.any.worker.html?wss": true, "bufferedAmount-unchanged-by-sync-xhr.any.html?default": false, "bufferedAmount-unchanged-by-sync-xhr.any.html?wpt_flags=h2": false, @@ -8162,13 +8158,9 @@ "send-many-64K-messages-with-backpressure.any.worker.html?wss": true, "stream": { "tentative": { - "abort.any.html?wpt_flags=h2": [ - "abort after connect should do nothing" - ], + "abort.any.html?wpt_flags=h2": true, "abort.any.html?wss": true, - "abort.any.worker.html?wpt_flags=h2": [ - "abort after connect should do nothing" - ], + "abort.any.worker.html?wpt_flags=h2": true, "abort.any.worker.html?wss": true, "backpressure-receive.any.html?wpt_flags=h2": false, "backpressure-receive.any.html?wss": true, @@ -8178,13 +8170,13 @@ "backpressure-send.any.html?wss": true, "backpressure-send.any.worker.html?wpt_flags=h2": false, "backpressure-send.any.worker.html?wss": true, - "close.any.html?wpt_flags=h2": false, + "close.any.html?wpt_flags=h2": true, "close.any.html?wss": true, - "close.any.worker.html?wpt_flags=h2": false, + "close.any.worker.html?wpt_flags=h2": true, "close.any.worker.html?wss": true, - "constructor.any.html?wpt_flags=h2": false, + "constructor.any.html?wpt_flags=h2": true, "constructor.any.html?wss": true, - "constructor.any.worker.html?wpt_flags=h2": false, + "constructor.any.worker.html?wpt_flags=h2": true, "constructor.any.worker.html?wss": true } }