diff --git a/.gitignore b/.gitignore index eab627b387d1e2..1511ab9d0f38d7 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,4 @@ junit.xml # Jupyter files .ipynb_checkpoints/ -Untitled*.ipynb \ No newline at end of file +Untitled*.ipynb diff --git a/Cargo.lock b/Cargo.lock index eaf124b1ae996d..ddaf9d53908968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1006,7 +1006,7 @@ dependencies = [ "quick-junit", "rand", "regex", - "ring", + "ring 0.17.7", "rustyline", "rustyline-derive", "serde", @@ -1119,7 +1119,7 @@ dependencies = [ "log", "once_cell", "parking_lot 0.12.1", - "ring", + "ring 0.17.7", "serde", "serde_json", "thiserror", @@ -1213,7 +1213,7 @@ dependencies = [ "p384", "p521", "rand", - "ring", + "ring 0.17.7", "rsa", "serde", "serde_bytes", @@ -1375,7 +1375,7 @@ dependencies = [ "phf", "pin-project", "rand", - "ring", + "ring 0.17.7", "scopeguard", "serde", "smallvec", @@ -1452,7 +1452,7 @@ version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd29f62e6dec60e585f579df3e9c2fc562aadf881319152974bc442a9042077" dependencies = [ - "ring", + "ring 0.17.7", "serde", "serde_json", "thiserror", @@ -1556,7 +1556,7 @@ dependencies = [ "rand", "regex", "reqwest", - "ring", + "ring 0.17.7", "ripemd", "rsa", "scrypt", @@ -1606,6 +1606,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "deno_quic" +version = "0.1.0" +dependencies = [ + "deno_core", + "deno_net", + "deno_tls", + "quinn", + "rustls", + "serde", +] + [[package]] name = "deno_runtime" version = "0.140.0" @@ -1627,6 +1639,7 @@ dependencies = [ "deno_napi", "deno_net", "deno_node", + "deno_quic", "deno_tls", "deno_url", "deno_web", @@ -1654,7 +1667,7 @@ dependencies = [ "ntapi", "once_cell", "regex", - "ring", + "ring 0.17.7", "rustyline", "serde", "signal-hook-registry", @@ -4615,6 +4628,54 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" +dependencies = [ + "bytes", + "rand", + "ring 0.16.20", + "rustc-hash", + "rustls", + "rustls-native-certs", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" +dependencies = [ + "bytes", + "libc", + "socket2 0.5.5", + "tracing", + "windows-sys 0.48.0", +] + [[package]] name = "quote" version = "1.0.35" @@ -4844,6 +4905,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.7" @@ -4854,7 +4930,7 @@ dependencies = [ "getrandom", "libc", "spin 0.9.8", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.48.0", ] @@ -4972,7 +5048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring", + "ring 0.17.7", "rustls-webpki", "sct", ] @@ -5016,8 +5092,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -5138,8 +5214,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -6139,7 +6215,7 @@ dependencies = [ "prost-build", "regex", "reqwest", - "ring", + "ring 0.17.7", "rustls-pemfile", "rustls-tokio-stream", "semver 1.0.14", @@ -6397,6 +6473,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -6657,6 +6734,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 36345b656fc584..1c756692f306cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "ext/kv", "ext/net", "ext/node", + "ext/quic", "ext/url", "ext/web", "ext/webgpu", @@ -69,6 +70,7 @@ deno_io = { version = "0.42.0", path = "./ext/io" } deno_net = { version = "0.124.0", path = "./ext/net" } deno_node = { version = "0.69.0", path = "./ext/node" } deno_kv = { version = "0.40.0", path = "./ext/kv" } +deno_quic = { version = "0.1.0", path = "./ext/quic" } deno_tls = { version = "0.119.0", path = "./ext/tls" } deno_url = { version = "0.132.0", path = "./ext/url" } deno_web = { version = "0.163.0", path = "./ext/web" } @@ -166,6 +168,7 @@ elliptic-curve = { version = "0.13.4", features = ["alloc", "arithmetic", "ecdh" p224 = { version = "0.13.0", features = ["ecdh"] } p256 = { version = "0.13.2", features = ["ecdh"] } p384 = { version = "0.13.0", features = ["ecdh"] } +quinn = "=0.10.2" # crypto rsa = { version = "0.9.3", default-features = false, features = ["std", "pem", "hazmat"] } # hazmat needed for PrehashSigner in ext/node @@ -250,6 +253,8 @@ opt-level = 3 opt-level = 3 [profile.bench.package.deno_net] opt-level = 3 +[profile.bench.package.deno_quic] +opt-level = 3 [profile.bench.package.deno_crypto] opt-level = 3 [profile.bench.package.deno_node] @@ -306,6 +311,8 @@ opt-level = 3 opt-level = 3 [profile.release.package.deno_net] opt-level = 3 +[profile.release.package.deno_quic] +opt-level = 3 [profile.release.package.deno_web] opt-level = 3 [profile.release.package.deno_crypto] diff --git a/cli/build.rs b/cli/build.rs index d3f428c508b98c..cac2e9581af542 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -158,6 +158,7 @@ mod ts { deno_broadcast_channel::get_declaration(), ); op_crate_libs.insert("deno.net", deno_net::get_declaration()); + op_crate_libs.insert("deno.quic", deno_quic::get_declaration()); // ensure we invalidate the build properly. for (_, path) in op_crate_libs.iter() { diff --git a/cli/js/40_testing.js b/cli/js/40_testing.js index 91d7f105fe9fc6..3508429af3a205 100644 --- a/cli/js/40_testing.js +++ b/cli/js/40_testing.js @@ -140,9 +140,9 @@ const OP_DETAILS = { "op_host_recv_message": ["receive a message from a web worker", "terminating a `Worker`"], "op_host_recv_ctrl": ["receive a message from a web worker", "terminating a `Worker`"], "op_webgpu_buffer_get_map_async": ["map a WebGPU buffer", "awaiting the result of a `GPUBuffer#mapAsync` call"], - "op_webgpu_request_adapter": ["request a WebGPU adapter", "awaiting the result of a `navigator.gpu.requestAdapter` call"], - "op_webgpu_request_device": ["request a WebGPU device", "awaiting the result of a `GPUAdapter#requestDevice` call"], - "op_ws_close": ["close a WebSocket", "awaiting until the `close` event is emitted on a `WebSocket`, or the `WebSocketStream#closed` promise resolves"], + "op_webgpu_request_adapter": ["request a WebGPU adapter", "awaiting the result of a `navigator.gpu.requestAdapter` call"], + "op_webgpu_request_device": ["request a WebGPU device", "awaiting the result of a `GPUAdapter#requestDevice` call"], + "op_ws_close": ["close a WebSocket", "awaiting until the `close` event is emitted on a `WebSocket`, or the `WebSocketStream#closed` promise resolves"], "op_ws_create": ["create a WebSocket", "awaiting until the `open` event is emitted on a `WebSocket`, or the result of a `WebSocketStream#connection` promise"], "op_ws_next_event": ["receive the next message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], "op_ws_send_text": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], @@ -319,6 +319,14 @@ function prettyResourceNames(name) { return ["A TCP listener", "opened", "closed"]; case "udpSocket": return ["A UDP socket", "opened", "closed"]; + case "quicListener": + return ["A QUIC listener", "opened", "closed"]; + case "quicConnection": + return ["A QUIC connection", "opened/accepted", "closed"]; + case "quicSendStream": + return ["A QUIC send stream", "created", "closed"]; + case "quicReceiveStream": + return ["A QUIC receive stream", "created", "closed"]; case "timer": return ["A timer", "started", "fired/cleared"]; case "textDecoder": @@ -386,6 +394,10 @@ function resourceCloseHint(name) { return "Close the TCP listener by calling `tcpListener.close()`."; case "udpSocket": return "Close the UDP socket by calling `udpSocket.close()`."; + case "quicListener": + return "Close the QUIC listener by calling `quicListener.close(..)`."; + case "quicConnection": + return "Close the QUIC connection by calling `quicConnection.close(..)`."; case "timer": return "Clear the timer by calling `clearInterval` or `clearTimeout`."; case "textDecoder": diff --git a/cli/main.rs b/cli/main.rs index 53c7bdf5a769e5..5b3cd0a00277d1 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -338,6 +338,11 @@ pub(crate) static UNSTABLE_GRANULAR_FLAGS: &[( "Enable unstable Web Worker APIs", 11, ), + ( + deno_runtime::deno_quic::UNSTABLE_FEATURE_NAME, + "Enable unstable QUIC API", + 12, + ), ]; pub(crate) fn unstable_exit_cb(_feature: &str, api_name: &str) { diff --git a/cli/tests/unit/quic_test.ts b/cli/tests/unit/quic_test.ts new file mode 100644 index 00000000000000..28718dbf0735a3 --- /dev/null +++ b/cli/tests/unit/quic_test.ts @@ -0,0 +1,108 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "./test_util.ts"; + +const cert = Deno.readTextFileSync("cli/tests/testdata/tls/localhost.crt"); +const key = Deno.readTextFileSync("cli/tests/testdata/tls/localhost.key"); +const caCerts = [Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem")]; + +async function pair(): Promise<[Deno.QuicConn, Deno.QuicConn]> { + const listener = await Deno.listenQuic({ + hostname: "localhost", + port: 0, + cert, + key, + alpnProtocols: ["deno-test"], + }); + + const [connA, connB] = await Promise.all([ + listener.accept(), + Deno.connectQuic({ + hostname: "localhost", + port: listener.addr.port, + caCerts, + alpnProtocols: ["deno-test"], + }), + ]); + + return [connA, connB]; +} + +Deno.test({ + name: "bidirectional stream", + sanitizeResources: false, + fn: async () => { + const [server, client] = await pair(); + + const encoded = (new TextEncoder()).encode("hi!"); + + { + const bi = await server.createBidirectionalStream({ sendOrder: 42 }); + assertEquals(bi.writable.sendOrder, 42); + bi.writable.sendOrder = 0; + assertEquals(bi.writable.sendOrder, 0); + await bi.writable.getWriter().write(encoded); + } + + { + const { value: bi } = await client.incomingBidirectionalStreams + .getReader() + .read(); + const { value: data } = await bi!.readable.getReader().read(); + assertEquals(data, encoded); + } + }, +}); + +Deno.test({ + name: "unidirectional stream", + sanitizeResources: false, + fn: async () => { + const [server, client] = await pair(); + + const encoded = (new TextEncoder()).encode("hi!"); + + { + const uni = await server.createUnidirectionalStream({ sendOrder: 42 }); + assertEquals(uni.sendOrder, 42); + uni.sendOrder = 0; + assertEquals(uni.sendOrder, 0); + await uni.getWriter().write(encoded); + } + + { + const { value: uni } = await client.incomingUnidirectionalStreams + .getReader() + .read(); + const { value: data } = await uni!.getReader().read(); + assertEquals(data, encoded); + } + }, +}); + +Deno.test({ + name: "datagrams", + sanitizeResources: false, + fn: async () => { + const [server, client] = await pair(); + + const encoded = (new TextEncoder()).encode("hi!"); + + await server.datagrams.writable.getWriter().write(encoded); + + const { value: data } = await client.datagrams.readable.getReader().read(); + assertEquals(data, encoded); + }, +}); + +Deno.test({ + name: "closing", + sanitizeResources: false, + fn: async () => { + const [server, client] = await pair(); + + server.close({ closeCode: 42, reason: "hi!" }); + + assertEquals(await client.closed, { closeCode: 42, reason: "hi!" }); + }, +}); diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 0cb917f9621857..8027954936911c 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -55,6 +55,14 @@ delete Object.prototype.__proto__; "openKv", "upgradeHttp", "umask", + "connectQuic", + "listenQuic", + "QuicBidirectionalStream", + "QuicDatagramDuplexStream", + "QuicConn", + "QuicListener", + "QuicReceiveStream", + "QuicSendStream", ]); const unstableMsgSuggestion = "If not, try changing the 'lib' compiler option to include 'deno.unstable' " + diff --git a/cli/tsc/dts/lib.deno.ns.d.ts b/cli/tsc/dts/lib.deno.ns.d.ts index a761c8d9a71190..7376d8d3a7f042 100644 --- a/cli/tsc/dts/lib.deno.ns.d.ts +++ b/cli/tsc/dts/lib.deno.ns.d.ts @@ -3,6 +3,7 @@ /// /// /// +/// /** Deno provides extra properties on `import.meta`. These are included here * to ensure that these are still available when using the Deno namespace in diff --git a/cli/tsc/mod.rs b/cli/tsc/mod.rs index 04450b8d097aea..ce1e67cdefb09b 100644 --- a/cli/tsc/mod.rs +++ b/cli/tsc/mod.rs @@ -97,6 +97,7 @@ pub fn get_types_declaration_file_text() -> String { "deno.crypto", "deno.broadcast_channel", "deno.net", + "deno.quic", "deno.shared_globals", "deno.cache", "deno.window", diff --git a/ext/quic/01_quic.js b/ext/quic/01_quic.js new file mode 100644 index 00000000000000..1cab0f5e4f96be --- /dev/null +++ b/ext/quic/01_quic.js @@ -0,0 +1,254 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { core, primordials } from "ext:core/mod.js"; +const { + op_quic_listen, + op_quic_accept, + op_quic_connect, + op_quic_accept_bi, + op_quic_accept_uni, + op_quic_open_bi, + op_quic_open_uni, + op_quic_open_datagrams, + op_quic_close_endpoint, + op_quic_close_connection, + op_quic_connection_closed, + op_quic_max_datagram_size, + op_quic_get_send_stream_priority, + op_quic_set_send_stream_priority, +} = core.ensureFastOps(); +const { + SymbolAsyncIterator, +} = primordials; +import { + getWritableStreamResourceBacking, + ReadableStream, + readableStreamForRid, + WritableStream, + writableStreamForRid, +} from "ext:deno_web/06_streams.js"; + +class QuicSendStream extends WritableStream { + get sendOrder() { + return op_quic_get_send_stream_priority( + getWritableStreamResourceBacking(this).rid, + ); + } + + set sendOrder(p) { + op_quic_set_send_stream_priority( + getWritableStreamResourceBacking(this).rid, + p, + ); + } +} + +class QuicReceiveStream extends ReadableStream {} + +class QuicDatagramDuplexStream { + #readable = null; + #writable = null; + + constructor(rid) { + const { 0: txRid, 1: rxRid } = op_quic_open_datagrams(rid); + this.#readable = readableStreamForRid(rxRid); + this.#writable = writableStreamForRid(txRid); + } + + get maxDatagramSize() { + return op_quic_max_datagram_size( + getWritableStreamResourceBacking(this.#writable).rid, + ); + } + + get readable() { + return this.#readable; + } + + get writable() { + return this.#writable; + } +} + +class QuicBidirectionalStream { + #readable = null; + #writable = null; + + constructor(txRid, rxRid) { + this.#readable = readableStreamForRid(rxRid, true, QuicReceiveStream); + this.#writable = writableStreamForRid(txRid, true, QuicSendStream); + } + + get readable() { + return this.#readable; + } + + get writable() { + return this.#writable; + } +} + +async function* bidiStream(rid) { + while (true) { + const r = await op_quic_accept_bi(rid); + if (r == null) { + break; + } + yield new QuicBidirectionalStream(r[0], r[1]); + } +} + +async function* uniStream(rid) { + while (true) { + const r = await op_quic_accept_uni(rid); + if (r == null) { + break; + } + yield readableStreamForRid(r, true, QuicReceiveStream); + } +} + +class QuicConn { + #rid = 0; + #protocol = null; + #bidiStream = null; + #uniStream = null; + #datagrams = null; + #closed = null; + + constructor(rid, protocol) { + this.#rid = rid; + this.#protocol = protocol; + } + + get rid() { + return this.#rid; + } + + async createBidirectionalStream({ sendOrder } = {}) { + const { 0: txRid, 1: rxRid } = await op_quic_open_bi(this.#rid); + if (sendOrder != null) { + op_quic_set_send_stream_priority(txRid, sendOrder); + } + return new QuicBidirectionalStream(txRid, rxRid); + } + + async createUnidirectionalStream({ sendOrder } = {}) { + const rid = await op_quic_open_uni(this.#rid); + if (sendOrder != null) { + op_quic_set_send_stream_priority(rid, sendOrder); + } + return writableStreamForRid(rid, true, QuicSendStream); + } + + get incomingBidirectionalStreams() { + if (this.#bidiStream == null) { + this.#bidiStream = ReadableStream.from(bidiStream(this.#rid)); + } + return this.#bidiStream; + } + + get incomingUnidirectionalStreams() { + if (this.#uniStream == null) { + this.#uniStream = ReadableStream.from(uniStream(this.#rid)); + } + return this.#uniStream; + } + + get datagrams() { + if (this.#datagrams == null) { + this.#datagrams = new QuicDatagramDuplexStream(this.#rid); + } + return this.#datagrams; + } + + get closed() { + if (this.#closed == null) { + this.#closed = op_quic_connection_closed(this.#rid); + } + return this.#closed; + } + + close({ closeCode, reason }) { + op_quic_close_connection(this.#rid, { closeCode, reason }); + } +} + +class QuicListener { + #rid = 0; + #addr = null; + + constructor(rid, addr) { + this.#rid = rid; + this.#addr = addr; + } + + get rid() { + return this.#rid; + } + + get addr() { + return this.#addr; + } + + async #accept() { + const { 0: rid, 1: protocol } = await op_quic_accept(this.#rid); + return new QuicConn(rid, protocol); + } + + async accept() { + const conn = await this.#accept(); + if (conn == null) { + throw new Deno.errors.BadResource("QuicListener has closed"); + } + return conn; + } + + async next() { + const conn = await this.#accept(); + if (conn == null) { + return { value: undefined, done: true }; + } + return { value: conn, done: false }; + } + + [SymbolAsyncIterator]() { + return this; + } + + close({ closeCode, reason }) { + op_quic_close_endpoint(this.#rid, { closeCode, reason }); + } +} + +async function listenQuic({ hostname, port, cert, key, alpnProtocols }) { + hostname = hostname || "0.0.0.0"; + const { 0: rid, 1: addr } = await op_quic_listen({ hostname, port }, { + cert, + key, + alpnProtocols, + }); + return new QuicListener(rid, addr); +} + +async function connectQuic( + { hostname, port, caCerts, certChain, privateKey, alpnProtocols }, +) { + const { 0: rid, 1: protocol } = await op_quic_connect({ hostname, port }, { + caCerts, + certChain, + privateKey, + alpnProtocols, + }); + return new QuicConn(rid, protocol); +} + +export { + connectQuic, + listenQuic, + QuicBidirectionalStream, + QuicConn, + QuicDatagramDuplexStream, + QuicListener, + QuicReceiveStream, + QuicSendStream, +}; diff --git a/ext/quic/Cargo.toml b/ext/quic/Cargo.toml new file mode 100644 index 00000000000000..de520c16810ae7 --- /dev/null +++ b/ext/quic/Cargo.toml @@ -0,0 +1,22 @@ +# Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_quic" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true +description = "QUIC for Deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core.workspace = true +deno_net.workspace = true +deno_tls.workspace = true +quinn.workspace = true +rustls.workspace = true +serde.workspace = true diff --git a/ext/quic/lib.deno_quic.d.ts b/ext/quic/lib.deno_quic.d.ts new file mode 100644 index 00000000000000..68d4d6b4f37486 --- /dev/null +++ b/ext/quic/lib.deno_quic.d.ts @@ -0,0 +1,154 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +/// +/// + +declare namespace Deno { + /** @category Network */ + export interface ListenQuicOptions { + /** The port to connect to. */ + port: number; + /** A literal IP address or host name that can be resolved to an IP address. */ + hostname?: string; + /** Server private key in PEM format */ + key: string; + /** Cert chain in PEM format */ + cert: string; + /** Application-Layer Protocol Negotiation (ALPN) protocols to announce to + * the client. QUIC requires the use of ALPN. + */ + alpnProtocols: string[]; + } + + /** Listen announces on the local transport address over QUIC. + * + * ```ts + * const lstnr = Deno.listenQuic({ port: 443, cert: "...", key: "..." }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function listenQuic(options: ListenQuicOptions): Promise; + + /** @category Network */ + export interface ConnectQuicOptions { + /** The port to connect to. */ + port: number; + /** A literal IP address or host name that can be resolved to an IP address. */ + hostname: string; + /** Application-Layer Protocol Negotiation (ALPN) protocols supported by + * the client. QUIC requires the use of ALPN. + */ + alpnProtocols: string[]; + /** A list of root certificates that will be used in addition to the + * default root certificates to verify the peer's certificate. + * + * Must be in PEM format. */ + caCerts?: string[]; + } + + /** Establishes a secure connection over QUIC using a hostname and port. The + * cert file is optional and if not included Mozilla's root certificates will + * be used (see also https://github.com/ctz/webpki-roots for specifics) + * + * ```ts + * const caCert = await Deno.readTextFile("./certs/my_custom_root_CA.pem"); + * const conn1 = await Deno.connectQuic({ hostname: "example.com", port: 443 }); + * const conn2 = await Deno.connectQuic({ caCerts: [caCert], hostname: "example.com", port: 443 }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function connectQuic(options: ConnectQuicOptions): Promise; + + /** @category Network */ + export interface QuicCloseInfo { + /** A number representing the error code for the error. */ + closeCode: number; + /** A string representing the reason for closing the connection. */ + reason: string; + } + + /** Specialized listener that accepts QUIC connections. + * + * @category Network + */ + export interface QuicListener extends AsyncIterable { + /** Return the rid of the `QuicListener`. */ + readonly rid: number; + /** Return the address of the `QuicListener`. */ + readonly addr: { hostname: string; port: number }; + + /** Waits for and resolves to the next connection to the `QuicListener`. */ + accept(): Promise; + /** Close closes the listener. Any pending accept promises will be rejected + * with errors. */ + close(info: QuicCloseInfo): void; + + [Symbol.asyncIterator](): AsyncIterableIterator; + } + + /** @category Network */ + export interface QuicConn extends Disposable { + /** Close closes the listener. Any pending accept promises will be rejected + * with errors. */ + close(info: QuicCloseInfo): void; + /** Opens and returns a bidirectional stream. */ + createBidirectionalStream( + options?: { sendOrder?: number }, + ): Promise; + /** Opens and returns a unidirectional stream. */ + createUnidirectionalStream( + options?: { sendOrder?: number }, + ): Promise; + + /** The resource ID of the connection. */ + readonly rid: number; + /** The negotiated ALPN protocol, if provided. */ + readonly protocol: string | undefined; + /** Returns a promise that resolves when the connection is closed. */ + readonly closed: Promise; + /** A stream of bidirectional streams opened by the peer. */ + readonly incomingBidirectionalStreams: ReadableStream< + QuicBidirectionalStream + >; + /** A stream of unidirectional streams opened by the peer. */ + readonly incomingUnidirectionalStreams: ReadableStream; + /** Returns the datagram stream for sending and receiving datagrams. */ + readonly datagrams: QuicDatagramDuplexStream; + } + + /** @category Network */ + export interface QuicBidirectionalStream { + /** Returns a QuicReceiveStream instance that can be used to read incoming data. */ + readonly readable: QuicReceiveStream; + /** Returns a QuicSendStream instance that can be used to write outgoing data. */ + readonly writable: QuicSendStream; + } + + /** @category Network */ + export interface QuicSendStream extends WritableStream { + /** Indicates the send priority of this stream relative to other streams for + * which the value has been set. */ + sendOrder: number; + } + + /** @category Network */ + // deno-lint-ignore no-empty-interface + export interface QuicReceiveStream extends ReadableStream {} + + /** @category Network */ + export interface QuicDatagramDuplexStream { + readonly maxDatagramSize: number; + /** Returns a ReadableStream instance that can be used to read incoming data. */ + readonly readable: ReadableStream; + /** Returns a WritableStream instance that can be used to read incoming data. */ + readonly writable: WritableStream; + } +} diff --git a/ext/quic/lib.rs b/ext/quic/lib.rs new file mode 100644 index 00000000000000..3a6a43868d26fb --- /dev/null +++ b/ext/quic/lib.rs @@ -0,0 +1,604 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::generic_error; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::op2; +use deno_core::AsyncRefCell; +use deno_core::AsyncResult; +use deno_core::BufView; +use deno_core::OpState; +use deno_core::RcRef; +use deno_core::Resource; +use deno_core::ResourceId; +use deno_core::WriteOutcome; +use deno_net::resolve_addr::resolve_addr; +use deno_net::DefaultTlsOptions; +use deno_net::NetPermissions; +use deno_net::UnsafelyIgnoreCertificateErrors; +use deno_tls::create_client_config; +use deno_tls::load_certs; +use deno_tls::load_private_keys; +use deno_tls::RootCertStoreProvider; +use deno_tls::SocketUse; +use serde::Deserialize; +use serde::Serialize; +use std::borrow::Cow; +use std::cell::RefCell; +use std::io::BufReader; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; + +pub fn get_declaration() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_quic.d.ts") +} + +pub const UNSTABLE_FEATURE_NAME: &str = "quic"; + +deno_core::extension!( + deno_quic, + // deps = [], + parameters = [ P: NetPermissions ], + ops = [ + op_quic_listen

, + op_quic_accept, + op_quic_connect

, + op_quic_accept_bi, + op_quic_accept_uni, + op_quic_open_bi, + op_quic_open_uni, + op_quic_open_datagrams, + op_quic_max_datagram_size, + op_quic_close_connection, + op_quic_close_endpoint, + op_quic_connection_closed, + op_quic_get_send_stream_priority, + op_quic_set_send_stream_priority, + ], + esm = ["01_quic.js"], + options = { + root_cert_store_provider: Option>, + unsafely_ignore_certificate_errors: Option>, + }, + state = |state, options| { + state.put(DefaultTlsOptions { + root_cert_store_provider: options.root_cert_store_provider, + }); + state.put(UnsafelyIgnoreCertificateErrors( + options.unsafely_ignore_certificate_errors, + )); + }, +); + +#[derive(Debug, Deserialize, Serialize)] +pub struct Addr { + pub hostname: String, + pub port: u16, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListenArgs { + cert: String, + key: String, + alpn_protocols: Option>, +} + +struct EndpointResource(quinn::Endpoint); + +impl Resource for EndpointResource { + fn name(&self) -> Cow { + "quicListener".into() + } +} + +#[op2(async)] +#[serde] +async fn op_quic_listen( + state: Rc>, + #[serde] addr: Addr, + #[serde] args: ListenArgs, +) -> Result<(ResourceId, Addr), AnyError> +where + NP: NetPermissions + 'static, +{ + state + .borrow_mut() + .borrow_mut::() + .check_net(&(&addr.hostname, Some(addr.port)), "Deno.connectQuic()")?; + + let cert_chain = load_certs(&mut BufReader::new(args.cert.as_bytes()))?; + let key_der = load_private_keys(args.key.as_bytes())?.remove(0); + + let addr = resolve_addr(&addr.hostname, addr.port) + .await? + .next() + .ok_or_else(|| generic_error("No resolved address found"))?; + + let mut crypto = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, key_der)?; + if let Some(alpn_protocols) = args.alpn_protocols { + crypto.alpn_protocols = alpn_protocols + .into_iter() + .map(|alpn| alpn.into_bytes()) + .collect(); + } + let config = quinn::ServerConfig::with_crypto(Arc::new(crypto)); + let endpoint = quinn::Endpoint::server(config, addr)?; + + let addr = endpoint.local_addr()?; + let addr = Addr { + hostname: format!("{}", addr.ip()), + port: addr.port(), + }; + + let rid = state + .borrow_mut() + .resource_table + .add(EndpointResource(endpoint)); + Ok((rid, addr)) +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CloseInfo { + close_code: u64, + reason: String, +} + +#[op2] +#[serde] +fn op_quic_close_endpoint( + state: Rc>, + #[smi] rid: ResourceId, + #[serde] info: CloseInfo, +) -> Result<(), AnyError> { + let endpoint = { + state + .borrow_mut() + .resource_table + .get::(rid)? + .0 + .clone() + }; + endpoint.close( + quinn::VarInt::from_u64(info.close_code)?, + info.reason.as_bytes(), + ); + Ok(()) +} + +struct ConnectionResource(quinn::Connection); + +impl Resource for ConnectionResource { + fn name(&self) -> Cow { + "quicConnection".into() + } + + fn read(self: Rc, _limit: usize) -> AsyncResult { + Box::pin(async move { + let data = self.0.read_datagram().await?; + Ok(BufView::from(data)) + }) + } + + fn write(self: Rc, view: BufView) -> AsyncResult { + Box::pin(async move { + let len = view.len(); + self.0.send_datagram(view.into())?; + Ok(WriteOutcome::Full { nwritten: len }) + }) + } +} + +#[op2(async)] +#[serde] +async fn op_quic_accept( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result)>, AnyError> { + let endpoint = { + state + .borrow_mut() + .resource_table + .get::(rid)? + .0 + .clone() + }; + match endpoint.accept().await { + Some(connecting) => { + let conn = connecting.await?; + let protocol = conn + .handshake_data() + .and_then(|h| h.downcast::().ok()) + .and_then(|h| h.protocol) + .map(|p| String::from_utf8_lossy(&p).into_owned()); + let rid = state + .borrow_mut() + .resource_table + .add(ConnectionResource(conn)); + Ok(Some((rid, protocol))) + } + None => Ok(None), + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectArgs { + ca_certs: Vec, + cert_chain: Option, + private_key: Option, + alpn_protocols: Option>, +} + +#[op2(async)] +#[serde] +async fn op_quic_connect( + state: Rc>, + #[serde] addr: Addr, + #[serde] args: ConnectArgs, +) -> Result<(ResourceId, Option), AnyError> +where + NP: NetPermissions + 'static, +{ + state + .borrow_mut() + .borrow_mut::() + .check_net(&(&addr.hostname, Some(addr.port)), "Deno.connectQuic()")?; + + let sock_addr = resolve_addr(&addr.hostname, addr.port) + .await? + .next() + .ok_or_else(|| generic_error("No resolved address found"))?; + + let root_cert_store = state + .borrow() + .borrow::() + .root_cert_store()?; + + let unsafely_ignore_certificate_errors = state + .borrow() + .try_borrow::() + .and_then(|it| it.0.clone()); + + let ca_certs = args + .ca_certs + .into_iter() + .map(|s| s.into_bytes()) + .collect::>(); + + let cert_chain_and_key = + if args.cert_chain.is_some() || args.private_key.is_some() { + let cert_chain = args + .cert_chain + .ok_or_else(|| type_error("No certificate chain provided"))?; + let private_key = args + .private_key + .ok_or_else(|| type_error("No private key provided"))?; + Some((cert_chain, private_key)) + } else { + None + }; + + let mut tls_config = create_client_config( + root_cert_store, + ca_certs, + unsafely_ignore_certificate_errors, + cert_chain_and_key, + SocketUse::GeneralSsl, + )?; + + if let Some(alpn_protocols) = args.alpn_protocols { + tls_config.alpn_protocols = + alpn_protocols.into_iter().map(|s| s.into_bytes()).collect(); + } + + let client_config = quinn::ClientConfig::new(Arc::new(tls_config)); + + let endpoint = quinn::Endpoint::client("[::]:0".parse()?)?; + let conn = endpoint + .connect_with(client_config, sock_addr, &addr.hostname)? + .await?; + + let protocol = conn + .handshake_data() + .and_then(|h| h.downcast::().ok()) + .and_then(|h| h.protocol) + .map(|p| String::from_utf8_lossy(&p).into_owned()); + + let rid = state + .borrow_mut() + .resource_table + .add(ConnectionResource(conn)); + Ok((rid, protocol)) +} + +#[op2] +#[serde] +fn op_quic_close_connection( + state: Rc>, + #[smi] rid: ResourceId, + #[serde] info: CloseInfo, +) -> Result<(), AnyError> { + let conn = { + state + .borrow_mut() + .resource_table + .get::(rid)? + .0 + .clone() + }; + conn.close( + quinn::VarInt::from_u64(info.close_code)?, + info.reason.as_bytes(), + ); + Ok(()) +} + +#[op2(async)] +#[serde] +async fn op_quic_connection_closed( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result { + let conn = { + state + .borrow_mut() + .resource_table + .get::(rid)? + .0 + .clone() + }; + let e = conn.closed().await; + match e { + quinn::ConnectionError::LocallyClosed => Ok(CloseInfo { + close_code: 0, + reason: "".into(), + }), + quinn::ConnectionError::ApplicationClosed(i) => Ok(CloseInfo { + close_code: i.error_code.into(), + reason: String::from_utf8_lossy(&i.reason).into_owned(), + }), + e => Err(e.into()), + } +} + +struct SendStreamResource(AsyncRefCell); + +impl SendStreamResource { + fn new(stream: quinn::SendStream) -> Self { + Self(AsyncRefCell::new(stream)) + } +} + +impl Resource for SendStreamResource { + fn name(&self) -> Cow { + "quicSendStream".into() + } + + fn write(self: Rc, view: BufView) -> AsyncResult { + Box::pin(async move { + let mut r = RcRef::map(self, |r| &r.0).borrow_mut().await; + let nwritten = r.write(&view).await?; + Ok(WriteOutcome::Partial { nwritten, view }) + }) + } +} + +struct RecvStreamResource(AsyncRefCell); + +impl RecvStreamResource { + fn new(stream: quinn::RecvStream) -> Self { + Self(AsyncRefCell::new(stream)) + } +} + +impl Resource for RecvStreamResource { + fn name(&self) -> Cow { + "quicReceiveStream".into() + } + + fn read(self: Rc, limit: usize) -> AsyncResult { + Box::pin(async move { + let mut r = RcRef::map(self, |r| &r.0).borrow_mut().await; + let mut data = vec![0; limit]; + let nread = r.read(&mut data).await?.unwrap_or(0); + data.truncate(nread); + Ok(BufView::from(data)) + }) + } +} + +#[op2(async)] +#[serde] +async fn op_quic_accept_bi( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result, AnyError> { + let conn = { + state + .borrow_mut() + .resource_table + .get::(rid)? + .0 + .clone() + }; + match conn.accept_bi().await { + Ok((tx, rx)) => { + let tx_rid = state + .borrow_mut() + .resource_table + .add(SendStreamResource::new(tx)); + let rx_rid = state + .borrow_mut() + .resource_table + .add(RecvStreamResource::new(rx)); + Ok(Some((tx_rid, rx_rid))) + } + Err(e) => match e { + quinn::ConnectionError::LocallyClosed => Ok(None), + quinn::ConnectionError::ApplicationClosed(..) => Ok(None), + _ => Err(e.into()), + }, + } +} + +#[op2(async)] +#[serde] +async fn op_quic_open_bi( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result<(ResourceId, ResourceId), AnyError> { + let conn = { + state + .borrow_mut() + .resource_table + .get::(rid)? + .0 + .clone() + }; + let (tx, rx) = conn.open_bi().await?; + let tx_rid = state + .borrow_mut() + .resource_table + .add(SendStreamResource::new(tx)); + let rx_rid = state + .borrow_mut() + .resource_table + .add(RecvStreamResource::new(rx)); + Ok((tx_rid, rx_rid)) +} + +#[op2(async)] +#[serde] +async fn op_quic_accept_uni( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result, AnyError> { + let conn = { + state + .borrow_mut() + .resource_table + .get::(rid)? + .0 + .clone() + }; + match conn.accept_uni().await { + Ok(rx) => { + let rid = state + .borrow_mut() + .resource_table + .add(RecvStreamResource::new(rx)); + Ok(Some(rid)) + } + Err(e) => match e { + quinn::ConnectionError::LocallyClosed => Ok(None), + quinn::ConnectionError::ApplicationClosed(..) => Ok(None), + _ => Err(e.into()), + }, + } +} + +#[op2(async)] +#[serde] +async fn op_quic_open_uni( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result { + let conn = { + state + .borrow_mut() + .resource_table + .get::(rid)? + .0 + .clone() + }; + let tx = conn.open_uni().await?; + let rid = state + .borrow_mut() + .resource_table + .add(SendStreamResource::new(tx)); + Ok(rid) +} + +#[op2] +#[serde] +fn op_quic_open_datagrams( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result<(ResourceId, ResourceId), AnyError> { + let conn = { + state + .borrow_mut() + .resource_table + .get::(rid)? + .0 + .clone() + }; + + let rid1 = state + .borrow_mut() + .resource_table + .add(ConnectionResource(conn.clone())); + let rid2 = state + .borrow_mut() + .resource_table + .add(ConnectionResource(conn)); + + Ok((rid1, rid2)) +} + +#[op2] +#[serde] +fn op_quic_max_datagram_size( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result { + let resource = state + .borrow_mut() + .resource_table + .get::(rid)?; + Ok(resource.0.max_datagram_size().unwrap_or(0)) +} + +#[op2] +#[serde] +fn op_quic_get_send_stream_priority( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result { + let resource = state + .borrow() + .resource_table + .get::(rid)?; + let r = RcRef::map(resource, |r| &r.0).try_borrow(); + match r { + Some(s) => Ok(s.priority()? as _), + None => Err(generic_error("Unable to get priority")), + } +} + +#[op2] +#[serde] +fn op_quic_set_send_stream_priority( + state: Rc>, + #[smi] rid: ResourceId, + priority: i32, +) -> Result<(), AnyError> { + let resource = state + .borrow() + .resource_table + .get::(rid)?; + let r = RcRef::map(resource, |r| &r.0).try_borrow(); + match r { + Some(s) => { + s.set_priority(priority)?; + Ok(()) + } + None => Err(generic_error("Unable to set priority")), + } +} diff --git a/ext/web/06_streams.js b/ext/web/06_streams.js index 976f623470d47e..480ab74a8775f4 100644 --- a/ext/web/06_streams.js +++ b/ext/web/06_streams.js @@ -915,8 +915,8 @@ const _original = Symbol("[[original]]"); * @param {boolean=} autoClose If the resource should be auto-closed when the stream closes. Defaults to true. * @returns {ReadableStream} */ -function readableStreamForRid(rid, autoClose = true) { - const stream = new ReadableStream(_brand); +function readableStreamForRid(rid, autoClose = true, Super) { + const stream = new (Super ?? ReadableStream)(_brand); stream[_resourceBacking] = { rid, autoClose }; const tryClose = () => { @@ -1137,8 +1137,8 @@ async function readableStreamCollectIntoUint8Array(stream) { * @param {boolean=} autoClose If the resource should be auto-closed when the stream closes. Defaults to true. * @returns {ReadableStream} */ -function writableStreamForRid(rid, autoClose = true) { - const stream = new WritableStream(_brand); +function writableStreamForRid(rid, autoClose = true, Super) { + const stream = new (Super ?? WritableStream)(_brand); stream[_resourceBacking] = { rid, autoClose }; const tryClose = () => { diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 18bad2d07856ae..ce4ef54155bf38 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -54,6 +54,7 @@ deno_io.workspace = true deno_net.workspace = true deno_node.workspace = true deno_kv.workspace = true +deno_quic.workspace = true deno_tls.workspace = true deno_url.workspace = true deno_web.workspace = true @@ -86,6 +87,7 @@ deno_kv.workspace = true deno_napi.workspace = true deno_net.workspace = true deno_node.workspace = true +deno_quic.workspace = true deno_tls.workspace = true deno_url.workspace = true deno_web.workspace = true diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index 97b4a9531f8651..8fd0d0c4e67e8a 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -25,6 +25,7 @@ import * as fsEvents from "ext:runtime/40_fs_events.js"; import * as process from "ext:runtime/40_process.js"; import * as signals from "ext:runtime/40_signals.js"; import * as tty from "ext:runtime/40_tty.js"; +import * as quic from "ext:deno_quic/01_quic.js"; // TODO(bartlomieju): this is funky we have two `http` imports import * as httpRuntime from "ext:runtime/40_http.js"; import * as kv from "ext:deno_kv/01_db.ts"; @@ -171,6 +172,7 @@ const unstableIds = { unsafeProto: 9, webgpu: 10, workerOptions: 11, + quic: 12, }; const denoNsUnstableById = {}; @@ -226,6 +228,17 @@ denoNsUnstableById[unstableIds.net] = { // denoNsUnstableById[unstableIds.workerOptions] = {} +denoNsUnstableById[unstableIds.quic] = { + connectQuic: quic.connectQuic, + listenQuic: quic.listenQuic, + QuicBidirectionalStream: quic.QuicBidirectionalStream, + QuicDatagramDuplexStream: quic.QuicDatagramDuplexStream, + QuicConn: quic.QuicConn, + QuicListener: quic.QuicListener, + QuicReceiveStream: quic.QuicReceiveStream, + QuicSendStream: quic.QuicSendStream, +}; + // when editing this list, also update unstableDenoProps in cli/tsc/99_main_compiler.js const denoNsUnstable = { listenDatagram: net.createListenDatagram( @@ -253,6 +266,14 @@ const denoNsUnstable = { KvU64: kv.KvU64, KvListIterator: kv.KvListIterator, cron: cron.cron, + connectQuic: quic.connectQuic, + listenQuic: quic.listenQuic, + QuicBidirectionalStream: quic.QuicBidirectionalStream, + QuicDatagramDuplexStream: quic.QuicDatagramDuplexStream, + QuicConn: quic.QuicConn, + QuicListener: quic.QuicListener, + QuicReceiveStream: quic.QuicReceiveStream, + QuicSendStream: quic.QuicSendStream, }; export { denoNs, denoNsUnstable, denoNsUnstableById, unstableIds }; diff --git a/runtime/lib.rs b/runtime/lib.rs index dbdce7850a48e7..160fa69f5f233b 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -15,6 +15,7 @@ pub use deno_kv; pub use deno_napi; pub use deno_net; pub use deno_node; +pub use deno_quic; pub use deno_tls; pub use deno_url; pub use deno_web; diff --git a/runtime/shared.rs b/runtime/shared.rs index 1f24fec0bb937b..f1796f72fbe444 100644 --- a/runtime/shared.rs +++ b/runtime/shared.rs @@ -30,7 +30,8 @@ extension!(runtime, deno_napi, deno_http, deno_io, - deno_fs + deno_fs, + deno_quic ], esm_entry_point = "ext:runtime/90_deno_ns.js", esm = [ diff --git a/runtime/snapshot.rs b/runtime/snapshot.rs index a50f0773abfe72..eb5708d358b61d 100644 --- a/runtime/snapshot.rs +++ b/runtime/snapshot.rs @@ -226,6 +226,7 @@ pub fn create_runtime_snapshot( ), deno_ffi::deno_ffi::init_ops_and_esm::(), deno_net::deno_net::init_ops_and_esm::(None, None), + deno_quic::deno_quic::init_ops_and_esm::(None, None), deno_tls::deno_tls::init_ops_and_esm(), deno_kv::deno_kv::init_ops_and_esm(deno_kv::sqlite::SqliteDbHandler::< Permissions, diff --git a/runtime/worker.rs b/runtime/worker.rs index 2cb1ab4915cce0..ec0d18e6cc33e5 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -377,6 +377,10 @@ impl MainWorker { options.root_cert_store_provider.clone(), options.unsafely_ignore_certificate_errors.clone(), ), + deno_quic::deno_quic::init_ops_and_esm::( + options.root_cert_store_provider.clone(), + options.unsafely_ignore_certificate_errors.clone(), + ), deno_tls::deno_tls::init_ops_and_esm(), deno_kv::deno_kv::init_ops_and_esm( MultiBackendDbHandler::remote_or_sqlite::( diff --git a/tools/core_import_map.json b/tools/core_import_map.json index 8122a2c84d35e1..edffd61df3e562 100644 --- a/tools/core_import_map.json +++ b/tools/core_import_map.json @@ -20,6 +20,7 @@ "ext:deno_kv/01_db.ts": "../ext/kv/01_db.ts", "ext:deno_net/01_net.js": "../ext/net/01_net.js", "ext:deno_net/02_tls.js": "../ext/net/02_tls.js", + "ext:deno_quic/01_net.js": "../ext/quic/01_quic.js", "ext:deno_node/_events.d.ts": "../ext/node/polyfills/_events.d.ts", "ext:deno_node/_fs/_fs_close.ts": "../ext/node/polyfills/_fs/_fs_close.ts", "ext:deno_node/_fs/_fs_common.ts": "../ext/node/polyfills/_fs/_fs_common.ts",