From 001fb6a0954f525077174aa2a8c2355bcccd8d4e Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 14 Jan 2024 20:17:44 -0800 Subject: [PATCH] quic --- .gitignore | 2 +- Cargo.lock | 109 +++++- Cargo.toml | 7 + cli/build.rs | 1 + cli/tsc/99_main_compiler.js | 7 + cli/tsc/dts/lib.deno.ns.d.ts | 1 + cli/tsc/mod.rs | 1 + ext/quic/01_quic.js | 310 ++++++++++++++++ ext/quic/Cargo.toml | 22 ++ ext/quic/lib.deno_quic.d.ts | 183 ++++++++++ ext/quic/lib.rs | 686 +++++++++++++++++++++++++++++++++++ ext/web/06_streams.js | 8 +- runtime/Cargo.toml | 2 + runtime/js/90_deno_ns.js | 27 +- runtime/lib.rs | 14 +- runtime/shared.rs | 3 +- runtime/snapshot.rs | 1 + runtime/worker.rs | 4 + tests/unit/quic_test.ts | 142 ++++++++ tools/core_import_map.json | 1 + 20 files changed, 1503 insertions(+), 28 deletions(-) create mode 100644 ext/quic/01_quic.js create mode 100644 ext/quic/Cargo.toml create mode 100644 ext/quic/lib.deno_quic.d.ts create mode 100644 ext/quic/lib.rs create mode 100644 tests/unit/quic_test.ts diff --git a/.gitignore b/.gitignore index 37f568324f1483..db7dd20d123ad0 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 89af50a8a4f019..4836bab22be1d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1067,7 +1067,7 @@ dependencies = [ "quick-junit", "rand", "regex", - "ring", + "ring 0.17.7", "rustyline", "rustyline-derive", "serde", @@ -1182,7 +1182,7 @@ dependencies = [ "log", "once_cell", "parking_lot 0.12.1", - "ring", + "ring 0.17.7", "serde", "serde_json", "thiserror", @@ -1295,7 +1295,7 @@ dependencies = [ "p384", "p521", "rand", - "ring", + "ring 0.17.7", "rsa", "serde", "serde_bytes", @@ -1458,7 +1458,7 @@ dependencies = [ "phf", "pin-project", "rand", - "ring", + "ring 0.17.7", "scopeguard", "serde", "smallvec", @@ -1535,7 +1535,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f348633cc4425b2a9011436e256b1ae8f6c8026ec2705d852baee8643dc5562" dependencies = [ - "ring", + "ring 0.17.7", "serde", "serde_json", "thiserror", @@ -1639,7 +1639,7 @@ dependencies = [ "rand", "regex", "reqwest", - "ring", + "ring 0.17.7", "ripemd", "rsa", "scrypt", @@ -1689,6 +1689,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.145.0" @@ -1711,6 +1723,7 @@ dependencies = [ "deno_napi", "deno_net", "deno_node", + "deno_quic", "deno_terminal", "deno_tls", "deno_url", @@ -1739,7 +1752,7 @@ dependencies = [ "ntapi", "once_cell", "regex", - "ring", + "ring 0.17.7", "rustyline", "serde", "signal-hook-registry", @@ -4789,6 +4802,53 @@ 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", + "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" @@ -5018,6 +5078,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" @@ -5028,7 +5103,7 @@ dependencies = [ "getrandom", "libc", "spin 0.9.8", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.48.0", ] @@ -5146,7 +5221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring", + "ring 0.17.7", "rustls-webpki", "sct", ] @@ -5190,8 +5265,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]] @@ -5312,8 +5387,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]] @@ -6324,7 +6399,7 @@ dependencies = [ "prost-build", "regex", "reqwest", - "ring", + "ring 0.17.7", "rustls-pemfile", "rustls-tokio-stream", "semver 1.0.14", @@ -6843,6 +6918,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 d069d1652abce5..9025d91decdfc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "ext/napi", "ext/net", "ext/node", + "ext/quic", "ext/url", "ext/web", "ext/webgpu", @@ -81,6 +82,7 @@ deno_webgpu = { version = "0.104.0", path = "./ext/webgpu" } deno_webidl = { version = "0.137.0", path = "./ext/webidl" } deno_websocket = { version = "0.142.0", path = "./ext/websocket" } deno_webstorage = { version = "0.132.0", path = "./ext/webstorage" } +deno_quic = { version = "0.1.0", path = "./ext/quic" } aes = "=0.8.3" anyhow = "1.0.57" @@ -172,6 +174,7 @@ url = { version = "2.3.1", features = ["serde", "expose_internals"] } uuid = { version = "1.3.0", features = ["v4"] } webpki-roots = "0.25.2" zstd = "=0.12.4" +quinn = { version = "=0.10.2", default-features = false, features = ["runtime-tokio", "tls-rustls"] } # crypto hkdf = "0.12.3" @@ -256,6 +259,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] @@ -312,6 +317,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 5fd6ca4d50e68b..c63451dae3f76b 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -159,6 +159,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/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 32c3bf03510150..8bf365ff48ce3d 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -54,6 +54,13 @@ delete Object.prototype.__proto__; "listenDatagram", "openKv", "umask", + "connectQuic", + "listenQuic", + "QuicBidirectionalStream", + "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 d17c31097757fc..b913100805781e 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 18316b750eca29..95054ee02f35e5 100644 --- a/cli/tsc/mod.rs +++ b/cli/tsc/mod.rs @@ -98,6 +98,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..06e882acb34ebc --- /dev/null +++ b/ext/quic/01_quic.js @@ -0,0 +1,310 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { core, primordials } from "ext:core/mod.js"; +import { + 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_send_datagram, + op_quic_read_datagram, + 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, + op_quic_get_conn_remote_addr, +} from "ext:core/ops"; +import { + getWritableStreamResourceBacking, + ReadableStream, + readableStreamForRid, + WritableStream, + writableStreamForRid, +} from "ext:deno_web/06_streams.js"; +const { + BadResourcePrototype, +} = core; +const { + Uint8Array, + TypedArrayPrototypeSubarray, + SymbolAsyncIterator, + SafePromisePrototypeFinally, + ObjectPrototypeIsPrototypeOf, +} = primordials; + +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 {} + +function readableStream(rid, closed) { + // stream can be indirectly closed by closing connection. + SafePromisePrototypeFinally(closed, () => { + core.tryClose(rid); + }); + return readableStreamForRid(rid, true, QuicReceiveStream); +} + +function writableStream(rid, closed) { + // stream can be indirectly closed by closing connection. + SafePromisePrototypeFinally(closed, () => { + core.tryClose(rid); + }); + return writableStreamForRid(rid, true, QuicSendStream); +} + +class QuicBidirectionalStream { + #readable = null; + #writable = null; + + constructor(txRid, rxRid, closed) { + this.#readable = readableStream(rxRid, closed); + this.#writable = writableStream(txRid, closed); + } + + get readable() { + return this.#readable; + } + + get writable() { + return this.#writable; + } +} + +async function* bidiStream(rid, closed) { + try { + while (true) { + const r = await op_quic_accept_bi(rid); + yield new QuicBidirectionalStream(r[0], r[1], closed); + } + } catch (error) { + if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) { + return; + } + throw error; + } +} + +async function* uniStream(rid, closed) { + try { + while (true) { + const uniRid = await op_quic_accept_uni(rid); + yield readableStream(uniRid, closed); + } + } catch (error) { + if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) { + return; + } + throw error; + } +} + +class QuicConn { + #rid = 0; + #protocol = null; + #bidiStream = null; + #uniStream = null; + #closed = null; + + constructor(rid, protocol) { + this.#rid = rid; + this.#protocol = protocol; + + this.#closed = op_quic_connection_closed(this.#rid); + core.unrefOpPromise(this.#closed); + + // connection can be indirectly closed by closing listener. + SafePromisePrototypeFinally(this.#closed, () => { + core.tryClose(rid); + }); + } + + get protocol() { + return this.#protocol; + } + + get remoteAddr() { + return op_quic_get_conn_remote_addr(this.#rid); + } + + async createBidirectionalStream({ sendOrder, waitUntilAvailable } = {}) { + const { 0: txRid, 1: rxRid } = await op_quic_open_bi( + this.#rid, + waitUntilAvailable ?? false, + ); + if (sendOrder != null) { + op_quic_set_send_stream_priority(txRid, sendOrder); + } + return new QuicBidirectionalStream(txRid, rxRid, this.#closed); + } + + async createUnidirectionalStream({ sendOrder, waitUntilAvailable } = {}) { + const rid = await op_quic_open_uni(this.#rid, waitUntilAvailable ?? false); + if (sendOrder != null) { + op_quic_set_send_stream_priority(rid, sendOrder); + } + return writableStream(rid, this.#closed); + } + + get incomingBidirectionalStreams() { + if (this.#bidiStream == null) { + this.#bidiStream = ReadableStream.from( + bidiStream(this.#rid, this.#closed), + ); + } + return this.#bidiStream; + } + + get incomingUnidirectionalStreams() { + if (this.#uniStream == null) { + this.#uniStream = ReadableStream.from(uniStream(this.#rid, this.#closed)); + } + return this.#uniStream; + } + + get maxDatagramSize() { + return op_quic_max_datagram_size(this.#rid); + } + + async readDatagram(p) { + const view = p || new Uint8Array(this.maxDatagramSize); + const nread = await op_quic_read_datagram(this.#rid, view); + return TypedArrayPrototypeSubarray(view, 0, nread); + } + + async sendDatagram(data) { + await op_quic_send_datagram(this.#rid, data); + } + + get closed() { + core.refOpPromise(this.#closed); + 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 addr() { + return this.#addr; + } + + async accept() { + const { 0: rid, 1: protocol } = await op_quic_accept(this.#rid); + return new QuicConn(rid, protocol); + } + + async next() { + let conn; + try { + conn = await this.accept(); + } catch (error) { + if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) { + return { value: undefined, done: true }; + } + throw error; + } + 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, + keepAliveInterval, + maxIdleTimeout, + maxConcurrentBidirectionalStreams, + maxConcurrentUnidirectionalStreams, + }, +) { + hostname = hostname || "0.0.0.0"; + const { 0: rid, 1: addr } = await op_quic_listen({ hostname, port }, { + cert, + key, + alpnProtocols, + }, { + keepAliveInterval, + maxIdleTimeout, + maxConcurrentBidirectionalStreams, + maxConcurrentUnidirectionalStreams, + }); + return new QuicListener(rid, addr); +} + +async function connectQuic( + { + hostname, + port, + serverName, + caCerts, + certChain, + privateKey, + alpnProtocols, + keepAliveInterval, + maxIdleTimeout, + maxConcurrentBidirectionalStreams, + maxConcurrentUnidirectionalStreams, + }, +) { + const { 0: rid, 1: protocol } = await op_quic_connect({ hostname, port }, { + caCerts, + certChain, + privateKey, + alpnProtocols, + serverName, + }, { + keepAliveInterval, + maxIdleTimeout, + maxConcurrentBidirectionalStreams, + maxConcurrentUnidirectionalStreams, + }); + return new QuicConn(rid, protocol); +} + +export { + connectQuic, + listenQuic, + QuicBidirectionalStream, + QuicConn, + 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..90c91ae80f36ac --- /dev/null +++ b/ext/quic/lib.deno_quic.d.ts @@ -0,0 +1,183 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +/// +/// + +declare namespace Deno { + /** @category Network */ + export interface QuicTransportOptions { + /** Period of inactivity before sending a keep-alive packet. Keep-alive + * packets prevent an inactive but otherwise healthy connection from timing + * out. Only one side of any given connection needs keep-alive enabled for + * the connection to be preserved. + */ + keepAliveInterval?: number; + /** Maximum duration of inactivity to accept before timing out the + * connection. The true idle timeout is the minimum of this and the peer’s + * own max idle timeout. + */ + maxIdleTimeout?: number; + /** Maximum number of incoming bidirectional streams that may be open + * concurrently. + */ + maxConcurrentBidirectionalStreams?: number; + /** Maximum number of incoming unidirectional streams that may be open + * concurrently. + */ + maxConcurrentUnidirectionalStreams?: number; + } + + /** @category Network */ + export interface ListenQuicOptions extends QuicTransportOptions { + /** 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 = await Deno.listenQuic({ port: 443, cert: "...", key: "...", alpnProtocols: ["h3"] }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function listenQuic(options: ListenQuicOptions): Promise; + + /** @category Network */ + export interface ConnectQuicOptions extends QuicTransportOptions { + /** The port to connect to. */ + port: number; + /** A literal IP address or host name that can be resolved to an IP address. */ + hostname: string; + /** The name used for validating the certificate provided by the server. If + * not provided, defaults to `hostname`. */ + serverName?: string | undefined; + /** 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, alpnProtocols: ["h3"] }); + * const conn2 = await Deno.connectQuic({ caCerts: [caCert], hostname: "example.com", port: 443, alpnProtocols: ["h3"] }); + * ``` + * + * 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 address of the `QuicListener`. */ + readonly addr: NetAddr; + + /** 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 QuicSendStreamOptions { + sendOrder?: number; + waitUntilAvailable?: boolean; + } + + /** @category Network */ + export interface QuicConn { + /** 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?: QuicSendStreamOptions, + ): Promise; + /** Opens and returns a unidirectional stream. */ + createUnidirectionalStream( + options?: QuicSendStreamOptions, + ): Promise; + /** Send a datagram. The provided data cannot be larger than + * `maxDatagramSize`. */ + sendDatagram(data: Uint8Array): Promise; + /** Receive a datagram. If no buffer is provider, one will be allocated. + * The zie of the provided buffer should be at least `maxDatagramSize`. */ + readDatagram(buffer?: Uint8Array): Promise; + + /** Return the remote address for the connection. Clients may change + * addresses at will, e.g. when switching to a cellular internet connection. + */ + readonly remoteAddr: NetAddr; + /** 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 maxDatagramSize: number; + } + + /** @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 {} +} diff --git a/ext/quic/lib.rs b/ext/quic/lib.rs new file mode 100644 index 00000000000000..8fb12648596f76 --- /dev/null +++ b/ext/quic/lib.rs @@ -0,0 +1,686 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::bad_resource; +use deno_core::error::generic_error; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::futures::task::noop_waker_ref; +use deno_core::op2; +use deno_core::AsyncRefCell; +use deno_core::AsyncResult; +use deno_core::BufView; +use deno_core::JsBuffer; +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::future::Future; +use std::io::BufReader; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::net::Ipv6Addr; +use std::path::PathBuf; +use std::pin::pin; +use std::rc::Rc; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; +use std::time::Duration; + +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_max_datagram_size, + op_quic_send_datagram, + op_quic_read_datagram, + op_quic_close_connection, + op_quic_close_endpoint, + op_quic_connection_closed, + op_quic_get_send_stream_priority, + op_quic_set_send_stream_priority, + op_quic_get_conn_remote_addr, + ], + 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)] +struct Addr { + hostname: String, + port: u16, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ListenArgs { + cert: String, + key: String, + alpn_protocols: Option>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TransportConfig { + keep_alive_interval: Option, + max_idle_timeout: Option, + max_concurrent_bidirectional_streams: Option, + max_concurrent_unidirectional_streams: Option, +} + +impl TryInto for TransportConfig { + type Error = AnyError; + + fn try_into(self) -> Result { + let mut cfg = quinn::TransportConfig::default(); + + if let Some(interval) = self.keep_alive_interval { + cfg.keep_alive_interval(Some(Duration::from_millis(interval))); + } + + if let Some(timeout) = self.max_idle_timeout { + cfg.max_idle_timeout(Some(Duration::from_millis(timeout).try_into()?)); + } + + if let Some(max) = self.max_concurrent_bidirectional_streams { + cfg.max_concurrent_bidi_streams(max.into()); + } + + if let Some(max) = self.max_concurrent_unidirectional_streams { + cfg.max_concurrent_uni_streams(max.into()); + } + + Ok(cfg) + } +} + +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, + #[serde] transport_config: TransportConfig, +) -> Result<(ResourceId, Addr), AnyError> +where + NP: NetPermissions + 'static, +{ + state + .borrow_mut() + .borrow_mut::() + .check_net(&(&addr.hostname, Some(addr.port)), "Deno.listenQuic()")?; + + 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 mut config = quinn::ServerConfig::with_crypto(Arc::new(crypto)); + config.transport_config(Arc::new(transport_config.try_into()?)); + 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(fast)] +fn op_quic_close_endpoint( + state: Rc>, + #[smi] rid: ResourceId, + #[bigint] close_code: u64, + #[string] reason: String, +) -> Result<(), AnyError> { + let endpoint = state + .borrow_mut() + .resource_table + .take::(rid)? + .0 + .clone(); + endpoint.close(quinn::VarInt::from_u64(close_code)?, reason.as_bytes()); + Ok(()) +} + +struct ConnectionResource(quinn::Connection); + +impl Resource for ConnectionResource { + fn name(&self) -> Cow { + "quicConnection".into() + } +} + +#[op2(async)] +#[serde] +async fn op_quic_accept( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result<(ResourceId, Option), AnyError> { + let endpoint = { + state + .borrow() + .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((rid, protocol)) + } + None => Err(bad_resource("QuicListener is closed")), + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ConnectArgs { + ca_certs: Option>, + cert_chain: Option, + private_key: Option, + alpn_protocols: Option>, + server_name: Option, +} + +#[op2(async)] +#[serde] +async fn op_quic_connect( + state: Rc>, + #[serde] addr: Addr, + #[serde] args: ConnectArgs, + #[serde] transport_config: TransportConfig, +) -> 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 + .unwrap_or_default() + .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 mut client_config = quinn::ClientConfig::new(Arc::new(tls_config)); + client_config.transport_config(Arc::new(transport_config.try_into()?)); + + let local_addr = match sock_addr.ip() { + IpAddr::V4(_) => IpAddr::from(Ipv4Addr::new(0, 0, 0, 0)), + IpAddr::V6(_) => IpAddr::from(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), + }; + + let conn = quinn::Endpoint::client((local_addr, 0).into())? + .connect_with( + client_config, + sock_addr, + &args.server_name.unwrap_or(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(fast)] +fn op_quic_close_connection( + state: Rc>, + #[smi] rid: ResourceId, + #[bigint] close_code: u64, + #[string] reason: String, +) -> Result<(), AnyError> { + let conn = { + state + .borrow() + .resource_table + .get::(rid)? + .0 + .clone() + }; + conn.close(quinn::VarInt::from_u64(close_code)?, reason.as_bytes()); + Ok(()) +} + +#[op2(async)] +#[serde] +async fn op_quic_connection_closed( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result { + let conn = { + state + .borrow() + .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<(ResourceId, ResourceId), AnyError> { + let conn = { + state + .borrow() + .resource_table + .get::(rid)? + .0 + .clone() + }; + match conn.accept_bi().await { + Ok((tx, rx)) => { + let mut state = state.borrow_mut(); + let tx_rid = state.resource_table.add(SendStreamResource::new(tx)); + let rx_rid = state.resource_table.add(RecvStreamResource::new(rx)); + Ok((tx_rid, rx_rid)) + } + Err(e) => match e { + quinn::ConnectionError::LocallyClosed + | quinn::ConnectionError::ApplicationClosed(..) => { + Err(bad_resource("QuicConn is closed")) + } + _ => Err(e.into()), + }, + } +} + +#[op2(async)] +#[serde] +async fn op_quic_open_bi( + state: Rc>, + #[smi] rid: ResourceId, + wait_for_available: bool, +) -> Result<(ResourceId, ResourceId), AnyError> { + let conn = { + state + .borrow() + .resource_table + .get::(rid)? + .0 + .clone() + }; + let (tx, rx) = if wait_for_available { + conn.open_bi().await? + } else { + let waker = noop_waker_ref(); + let mut cx = Context::from_waker(waker); + match pin!(conn.open_bi()).poll(&mut cx) { + Poll::Ready(r) => r?, + Poll::Pending => { + return Err(generic_error("Connection has reached the maximum number of outgoing concurrent bidirectional streams")); + } + } + }; + let mut state = state.borrow_mut(); + let tx_rid = state.resource_table.add(SendStreamResource::new(tx)); + let rx_rid = state.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 { + let conn = { + state + .borrow() + .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(rid) + } + Err(e) => match e { + quinn::ConnectionError::LocallyClosed + | quinn::ConnectionError::ApplicationClosed(..) => { + Err(bad_resource("QuicConn is closed")) + } + _ => Err(e.into()), + }, + } +} + +#[op2(async)] +#[serde] +async fn op_quic_open_uni( + state: Rc>, + #[smi] rid: ResourceId, + wait_for_available: bool, +) -> Result { + let conn = { + state + .borrow() + .resource_table + .get::(rid)? + .0 + .clone() + }; + let tx = if wait_for_available { + conn.open_uni().await? + } else { + let waker = noop_waker_ref(); + let mut cx = Context::from_waker(waker); + match pin!(conn.open_uni()).poll(&mut cx) { + Poll::Ready(r) => r?, + Poll::Pending => { + return Err(generic_error("Connection has reached the maximum number of outgoing concurrent unidirectional streams")); + } + } + }; + let rid = state + .borrow_mut() + .resource_table + .add(SendStreamResource::new(tx)); + Ok(rid) +} + +#[op2(async)] +async fn op_quic_send_datagram( + state: Rc>, + #[smi] rid: ResourceId, + #[buffer] zero_copy: JsBuffer, +) -> Result<(), AnyError> { + let conn = { + state + .borrow() + .resource_table + .get::(rid)? + .0 + .clone() + }; + // TODO: https://github.com/quinn-rs/quinn/issues/1738 + conn.send_datagram(zero_copy.into())?; + Ok(()) +} + +#[op2(async)] +async fn op_quic_read_datagram( + state: Rc>, + #[smi] rid: ResourceId, + #[buffer] mut buf: JsBuffer, +) -> Result { + let conn = { + state + .borrow() + .resource_table + .get::(rid)? + .0 + .clone() + }; + let data = conn.read_datagram().await?; + buf[0..data.len()].copy_from_slice(&data); + Ok(data.len() as _) +} + +#[op2(fast)] +fn op_quic_max_datagram_size( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result { + let resource = state + .borrow() + .resource_table + .get::(rid)?; + Ok(resource.0.max_datagram_size().unwrap_or(0) as _) +} + +#[op2(fast)] +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()?), + None => Err(generic_error("Unable to get priority")), + } +} + +#[op2(fast)] +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")), + } +} + +#[op2] +#[serde] +fn op_quic_get_conn_remote_addr( + state: Rc>, + #[smi] rid: ResourceId, +) -> Result { + let resource = state + .borrow() + .resource_table + .get::(rid)?; + let addr = resource.0.remote_address(); + Ok(Addr { + hostname: format!("{}", addr.ip()), + port: addr.port(), + }) +} diff --git a/ext/web/06_streams.js b/ext/web/06_streams.js index fa9e4b59eaa92c..28035dba43eaf3 100644 --- a/ext/web/06_streams.js +++ b/ext/web/06_streams.js @@ -913,8 +913,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 = () => { @@ -1135,8 +1135,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 408518975b616d..1a32a0eef94906 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -57,6 +57,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 @@ -90,6 +91,7 @@ deno_kv.workspace = true deno_napi.workspace = true deno_net.workspace = true deno_node.workspace = true +deno_quic.workspace = true deno_terminal.workspace = true deno_tls.workspace = true deno_url.workspace = true diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index 9f403e901e2a42..86d7575aed471a 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"; @@ -259,10 +260,11 @@ const unstableIds = { http: 5, kv: 6, net: 7, - temporal: 8, - unsafeProto: 9, - webgpu: 10, - workerOptions: 11, + quic: 8, + temporal: 9, + unsafeProto: 10, + webgpu: 11, + workerOptions: 12, }; const denoNsUnstableById = {}; @@ -319,6 +321,16 @@ denoNsUnstableById[unstableIds.webgpu] = { // denoNsUnstableById[unstableIds.workerOptions] = {} +denoNsUnstableById[unstableIds.quic] = { + connectQuic: quic.connectQuic, + listenQuic: quic.listenQuic, + QuicBidirectionalStream: quic.QuicBidirectionalStream, + 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( @@ -346,6 +358,13 @@ const denoNsUnstable = { KvU64: kv.KvU64, KvListIterator: kv.KvListIterator, cron: cron.cron, + connectQuic: quic.connectQuic, + listenQuic: quic.listenQuic, + QuicBidirectionalStream: quic.QuicBidirectionalStream, + 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 b63fd41340b538..3b883a43b3fd45 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -16,6 +16,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; @@ -87,28 +88,33 @@ pub static UNSTABLE_GRANULAR_FLAGS: &[( "Enable unstable net APIs", 7, ), + ( + deno_quic::UNSTABLE_FEATURE_NAME, + "Enable unstable QUIC API", + 8, + ), ( "temporal", "Enable unstable Temporal API", // Not used in JS - 8, + 9, ), ( "unsafe-proto", "Enable unsafe __proto__ support. This is a security risk.", // This number is used directly in the JS code. Search // for "unstableIds" to see where it's used. - 9, + 10, ), ( deno_webgpu::UNSTABLE_FEATURE_NAME, "Enable unstable `WebGPU` API", - 10, + 11, ), ( ops::worker_host::UNSTABLE_FEATURE_NAME, "Enable unstable Web Worker APIs", - 11, + 12, ), ]; diff --git a/runtime/shared.rs b/runtime/shared.rs index 04fcdcfdb725d8..efef6f94bb8a69 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 b23b024ee1358e..214d89a4d072d2 100644 --- a/runtime/snapshot.rs +++ b/runtime/snapshot.rs @@ -227,6 +227,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 b6aff3c1572818..9d0e3253dfe5a7 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -378,6 +378,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/tests/unit/quic_test.ts b/tests/unit/quic_test.ts new file mode 100644 index 00000000000000..e54734811b5948 --- /dev/null +++ b/tests/unit/quic_test.ts @@ -0,0 +1,142 @@ +// 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(opt?: Deno.QuicTransportOptions): Promise< + [Deno.QuicConn, Deno.QuicConn, Deno.QuicListener] +> { + const listener = await Deno.listenQuic({ + hostname: "localhost", + port: 0, + cert, + key, + alpnProtocols: ["deno-test"], + ...opt, + }); + + const [server, client] = await Promise.all([ + listener.accept(), + Deno.connectQuic({ + hostname: "localhost", + port: listener.addr.port, + caCerts, + alpnProtocols: ["deno-test"], + ...opt, + }), + ]); + + assertEquals(server.protocol, "deno-test"); + assertEquals(client.protocol, "deno-test"); + assertEquals(client.remoteAddr, listener.addr); + + return [server, client, listener]; +} + +Deno.test("bidirectional stream", async () => { + const [server, client, listener] = 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); + } + + listener.close({ closeCode: 0, reason: "" }); + client.close({ closeCode: 0, reason: "" }); +}); + +Deno.test("unidirectional stream", async () => { + const [server, client, listener] = 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); + } + + listener.close({ closeCode: 0, reason: "" }); + client.close({ closeCode: 0, reason: "" }); +}); + +Deno.test("datagrams", async () => { + const [server, client, listener] = await pair(); + + const encoded = (new TextEncoder()).encode("hi!"); + + await server.sendDatagram(encoded); + + const data = await client.readDatagram(); + assertEquals(data, encoded); + + listener.close({ closeCode: 0, reason: "" }); + client.close({ closeCode: 0, reason: "" }); +}); + +Deno.test("closing", async () => { + const [server, client, listener] = await pair(); + + server.close({ closeCode: 42, reason: "hi!" }); + + assertEquals(await client.closed, { closeCode: 42, reason: "hi!" }); + + listener.close({ closeCode: 0, reason: "" }); +}); + +Deno.test("max concurrent streams", async () => { + const [server, client, listener] = await pair({ + maxConcurrentBidirectionalStreams: 1, + maxConcurrentUnidirectionalStreams: 1, + }); + + { + await server.createBidirectionalStream(); + await server.createBidirectionalStream() + .then(() => { + throw new Error("expected failure"); + }, () => { + // success! + }); + } + + { + await server.createUnidirectionalStream(); + await server.createUnidirectionalStream() + .then(() => { + throw new Error("expected failure"); + }, () => { + // success! + }); + } + + listener.close({ closeCode: 0, reason: "" }); + server.close({ closeCode: 0, reason: "" }); + client.close({ closeCode: 0, reason: "" }); +}); diff --git a/tools/core_import_map.json b/tools/core_import_map.json index 9e70f52f7fbb06..25de4fa95a9561 100644 --- a/tools/core_import_map.json +++ b/tools/core_import_map.json @@ -21,6 +21,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",